diff --git a/android/app/build.gradle b/android/app/build.gradle index a32f9258f2..42509db7a3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -170,13 +170,13 @@ android { dimension "environment" applicationId "me.tinykitten.trainlcd.dev" versionNameSuffix "-dev" - versionCode 100000837 - versionName "10.18.0" + versionCode 100000851 + versionName "10.18.1" } prod { dimension "environment" - versionCode 100000837 - versionName "10.18.0" + versionCode 100000851 + versionName "10.18.1" } // ストア配信する dev (Canary) と applicationId を分けた、ローカル検証専用のフレーバー。 // ストアの Canary 版を消さずに同居させるためだけに存在し、配信には使わない @@ -186,8 +186,8 @@ android { dimension "environment" applicationId "me.tinykitten.trainlcd.local" versionNameSuffix "-local" - versionCode 100000837 - versionName "10.18.0" + versionCode 100000851 + versionName "10.18.1" } } } diff --git a/android/wearable/build.gradle.kts b/android/wearable/build.gradle.kts index bfae189095..afa2f42740 100644 --- a/android/wearable/build.gradle.kts +++ b/android/wearable/build.gradle.kts @@ -85,13 +85,13 @@ android { dimension = "environment" applicationIdSuffix = ".dev" versionNameSuffix = "-dev" - versionCode = 100000838 - versionName = "10.18.0" + versionCode = 100000852 + versionName = "10.18.1" } create("prod") { dimension = "environment" - versionCode = 100000838 - versionName = "10.18.0" + versionCode = 100000852 + versionName = "10.18.1" } // :app の local フレーバーと対になるローカル検証専用フレーバー。Wear の Data Layer は // applicationId が一致する場合しか疎通しないため、:app を .local にしたときは @@ -100,8 +100,8 @@ android { dimension = "environment" applicationIdSuffix = ".local" versionNameSuffix = "-local" - versionCode = 100000838 - versionName = "10.18.0" + versionCode = 100000852 + versionName = "10.18.1" } } diff --git a/app.config.ts b/app.config.ts index 879e9be054..3b55ebd914 100644 --- a/app.config.ts +++ b/app.config.ts @@ -8,7 +8,7 @@ const IS_DEV = process.env.APP_VARIANT === 'dev' || IS_LOCAL; export default { name: 'TrainLCD', slug: 'trainlcd', - version: '10.18.0', + version: '10.18.1', plugins: [ 'expo-image', 'expo-font', @@ -64,7 +64,7 @@ export default { userInterfaceStyle: 'automatic', // Expo SDK 57 の各モジュール(expo / expo-modules-core ほか)は podspec で iOS 16.4 以上を要求する deploymentTarget: '16.4', - buildNumber: '2948', + buildNumber: '2955', scheme: IS_DEV ? 'CanaryTrainLCD' : 'ProdTrainLCD', bundleIdentifier: IS_DEV ? 'me.tinykitten.trainlcd.dev' @@ -78,7 +78,7 @@ export default { ? 'me.tinykitten.trainlcd.dev' : 'me.tinykitten.trainlcd', permissions: [], - versionCode: 100000837, + versionCode: 100000851, }, owner: 'trainlcd', experiments: { diff --git a/assets/gpx/HitachiShinagawaSendai.gpx b/assets/gpx/HitachiShinagawaSendai.gpx new file mode 100644 index 0000000000..10074be020 --- /dev/null +++ b/assets/gpx/HitachiShinagawaSendai.gpx @@ -0,0 +1,55185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/translations/en.json b/assets/translations/en.json index 0a04781b6b..e3ac4939bd 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -399,7 +399,7 @@ "rideReviewPrefecturesShowAll": "Show all (%{count} more)", "rideReviewPrefecturesUnrecorded": "Rides whose prefectures are unknown (%{count}) are not included. These include rides recorded before prefectures were saved.", "rideReviewPrefecturesNone": "The prefectures of the rides in this period are unknown. These include rides recorded before prefectures were saved.", - "rideReviewLineValue": "%{distance} km · %{count} rides", + "rideReviewLineValue": "%{count} rides · %{distance} km", "rideReviewNote": "Distances are the sum of track lengths between the stations where an arrival was detected. Rides in auto mode are not included.", "rideReviewEmptyTitle": "No rides yet", "rideReviewEmptyBody": "Choose a line and ride, and the distance and time are recorded at each station you arrive at. Rides taken before you turned this on are not included.", @@ -420,7 +420,7 @@ "rideReviewWalkthroughRouteMapTitle": "Routes", "rideReviewWalkthroughRouteMapDescription": "The sections you rode are drawn on the map in each line's color. Open the map in full screen with the button at the top right to move it around.", "rideReviewWalkthroughTopLinesTitle": "Most ridden lines", - "rideReviewWalkthroughTopLinesDescription": "Lines are listed in order of the distance you rode on them in the period.", + "rideReviewWalkthroughTopLinesDescription": "Lines are listed by how many times you rode them in the period. Lines with the same count are ordered by distance.", "rideReviewTabIntroOpen": "Open ride review", "rideReviewRouteMap": "Routes", "rideReviewRouteMapTitle": "Routes: %{range}", diff --git a/assets/translations/ja.json b/assets/translations/ja.json index 6c7c0b44c6..dc3a3380e3 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -400,7 +400,7 @@ "rideReviewPrefecturesShowAll": "すべて表示(ほか %{count} 都道府県)", "rideReviewPrefecturesUnrecorded": "都道府県が分からない乗車(%{count}回)は、この集計に入れていません。都道府県を記録する前の乗車も、これに含まれます。", "rideReviewPrefecturesNone": "この期間の乗車は、どれも都道府県が分かりません。都道府県を記録する前の乗車も、これに含まれます。", - "rideReviewLineValue": "%{distance} km・%{count}回", + "rideReviewLineValue": "%{count}回・%{distance} km", "rideReviewNote": "到着を検出した駅のあいだを、線路の長さで合計しています。オートモードの乗車は含みません。", "rideReviewEmptyTitle": "まだ記録がありません", "rideReviewEmptyBody": "路線を選んで乗車すると、到着した駅ごとに距離と時間を記録します。振り返りを有効にする前の乗車は集計できません。", @@ -421,7 +421,7 @@ "rideReviewWalkthroughRouteMapTitle": "移動経路", "rideReviewWalkthroughRouteMapDescription": "乗った区間を、路線の色で地図に描きます。右上のボタンで全画面にすると、地図を動かせます。", "rideReviewWalkthroughTopLinesTitle": "よく乗った路線", - "rideReviewWalkthroughTopLinesDescription": "期間内に長い距離を乗った路線の順に並べます。", + "rideReviewWalkthroughTopLinesDescription": "期間内に乗った回数の多い路線の順に並べます。回数が同じときは、長い距離を乗った路線を先にします。", "rideReviewTabIntroOpen": "振り返りを見る", "rideReviewRouteMap": "移動経路", "rideReviewRouteMapTitle": "%{range}の移動経路", diff --git a/docs/changelog.md b/docs/changelog.md index 5f98f3b46b..401d966889 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,70 @@ CLAUDE.md「Security & Configuration Guardrails」に従い、依存更新後に 新しいエントリを上に追加する。 +## 2026-10-05 — ビルドに入っていなかった expo-task-manager のパッチを削除 + +対象バージョン: v10.18.0 時点の `dev` + +### 内容 + +- `patches/expo-task-manager+57.0.13.patch` を削除した。`TaskManagerUtils.createJobInfo` の API 31+ 分岐で + `setExpedited(true)` を `setMinimumLatency(0)` と `setOverrideDeadline` に置き換えるパッチだった。 +- `expo-task-manager` はビルド済みの AAR(`local-maven-repo`)を同梱し、`expo-module.config.json` に `publication` を持つ。 + `buildFromSource` に入っていないため、Gradle は AAR を使い、`node_modules` のソースへのパッチはビルドに入らない。 + このパッチを作った時点の 14.0.9 から 57.0.13 まで、どの版も同じ構成だった。 +- #5163 で `app.json` の `expo.android.autolinking.buildFromSource` に `expo-task-manager` を加えていた。 + `expo-modules-autolinking` が読むのは `package.json` の `expo.autolinking` だけなので、この設定は使われていなかった。 + その後 #5173 で `app.config.ts` へ移行したときに設定自体もなくなっている。 +- ストア版の本番 APK(v10.18.0)を逆アセンブルし、`createJobInfo` が上流のまま(API 31+ は `setExpedited(true)`)であることを確かめた。 + パッチを消しても、ビルドに入るコードは変わらない。 + +### 確認 + +- `./gradlew :app:dependencies --configuration prodReleaseRuntimeClasspath` で、 + `expo-task-manager` が `host.exp.exponent:expo.modules.taskmanager:57.0.13`(AAR)として解決されることを確かめた。 +- Sentry の直近 90 日間で、このパッチが対象にしていた `JobInfo.Builder.build()` の + `You're trying to build a job with no constraints` は 0 件だった。 + +### 学び + +- Expo のモジュールへのネイティブのパッチは、`package.json` の `expo.autolinking.android.buildFromSource` に加えないとビルドに入らない。 + `app.json` に書いても読まれない。 +- 「2026-08-26 — Expo SDK 57 系の依存を推奨バージョンへ更新」の項にある、このパッチの挙動の説明は、 + パッチがビルドに入っていた前提で書かれている。実際の出荷物は上流の実装のままだった。 + +## 2026-10-05 — Android で一覧から消して開き直すと端末内蔵 TTS が鳴らない不具合を修正 + +対象バージョン: v10.18.0 の本番で発生 + +### 内容 + +- 走行中にアプリを Android の「最近使ったアプリ」の一覧から消すと、MainActivity が破棄され、 + `expo-speech` の `OnActivityDestroys` が `TextToSpeech.shutdown()` を呼ぶ。プロセスはキャッシュとして残る。 +- 残ったプロセスのまま開き直すと、`expo-speech` は `by lazy` で作った shutdown 済みのインスタンスを使い続ける。 + `getVoices` と `speak` は `not bound to TTS engine` で失敗し続け、プロセスが終わるまで鳴らない。 +- Android の本番は Remote Config の `remote_tts_enabled_android=false` で端末内蔵 TTS を使うため、この経路に当たった。 +- `patches/expo-speech+57.0.1.patch` で、破棄したら参照を捨て、次に前面へ戻ってから作り直すようにした。 + 破棄から前面へ戻るまでは `speak` を受け付けず、一覧から消したアプリが読み上げないようにしている。 + TTS の状態は AsyncFunction と同じ `modulesQueue` のスレッドだけで扱い、ロックを使わない。 +- `expo-speech` は Android ではビルド済みの AAR(`local-maven-repo`)から取り込まれる。 + パッチをビルドに入れるため、`package.json` の `expo.autolinking.android.buildFromSource` に `expo-speech` を加えた。 +- 以前の `patches/expo-speech+57.0.1.patch`(#6457・#6804)も同じ理由でビルドに入っておらず、本番のバイナリに + 含まれていなかった。今回のパッチには含めていない。 + +### 確認 + +- `prodRelease`(署名だけ debug 鍵に差し替え)を Android 13 の実機に入れ、走行中に一覧から消して開き直す操作を 2 回行った。 + 消したあとの合成要求は 0 件で、開き直したあとは同じプロセスのまま日本語・英語とも合成された。 +- `buildFromSource` を外して作った同じ版では、同じ手順で `speak failed: not bound to TTS engine` になった。 +- `npm run lint`・`npm test`・`npm run typecheck` は成功した。 + +### 学び + +- patch-package で `node_modules` のネイティブのソースを書き換えても、Expo のモジュールがビルド済みの AAR を + 同梱していればビルドに入らない。ネイティブのパッチは `buildFromSource` に加え、APK の中にパッチの + コードがあることまで確かめる。 +- patch-package はパッチを作るときに `android/build` などの生成物も差分に含める。作る前に生成物を消す。 + ## 2026-09-23 — Android リリースビルドの Gradle デーモンのヒープ上限を 4096m に引き上げ 対象バージョン: v10.16.0 の本番ビルド失敗への対応 diff --git a/docs/location-simulation.md b/docs/location-simulation.md index 66c8034cf7..f08d1fadd0 100644 --- a/docs/location-simulation.md +++ b/docs/location-simulation.md @@ -253,6 +253,7 @@ npm run gpx:generate -- \ | `assets/gpx/SampleJY.gpx` | 山手線。外部ツールで記録した実走行ログ | | `assets/gpx/SampleTohokuShinkansen.gpx` | 東北新幹線 盛岡→仙台。最高 320km/h、一ノ関に停車、通過駅 5 駅を経由 | | `assets/gpx/KeioSpecialExpress.gpx` | 京王線 特急 新宿→京王八王子。到着時間推定に合わせて走る | +| `assets/gpx/HitachiShinagawaSendai.gpx` | 特急ひたち 品川→仙台。到着時間推定に合わせて走る | | `assets/gpx/KatamachiRapid.gpx` | 片町線 快速 京田辺→木津。駅間 2.3km 前後の全駅停車 | | `assets/gpx/SobuRapid.gpx` | 総武快速線 錦糸町→津田沼。駅間 3.6〜7.6km (線路の長さ) の全駅停車 | | `assets/gpx/FLinerSeibu.gpx` | Fライナー相当 元町・中華街→飯能。地下鉄の電波環境付き | @@ -266,6 +267,15 @@ npm run gpx:generate -- \ 同じコマンドを再実行するとバイト単位で同じ内容が得られる。京王線の特急は到着時間 推定の較正テーブルに無いので、所要時間は一般則による推定値になる。 +`HitachiShinagawaSendai.gpx` は、通過駅が長く続く在来線特急のサンプル。東海道線・上野東京ライン・ +常磐快速線・常磐線の 4 路線を直通し、経路 94 駅のうち 30 駅に停車する。`--line-group 116` で +生成した。約 307 分・18394 点と同梱のサンプルで最も長い。巡航速度は到着時間推定に合わせて +決まり、最高でも 82km/h にとどまる。 + +停車駅は StationAPI の `stopCondition` に従う。列車によって停まる駅は一部停車 (`PartialStop`)・ +一部通過 (`Partial`) になっており、生成時はどちらも停車として扱う (柏・土浦など 9 駅)。 +そのため、実在するどの 1 本の停車パターンとも一致しない。 + `FLinerSeibu.gpx` は地下鉄の電波環境を持つ唯一のサンプル。いわゆる F ライナーだが、 StationAPI に「F ライナー」という種別は無く、みなとみらい線・東急東横線が特急、 副都心線が急行、西武有楽町線・西武池袋線が快速急行という組み合わせ (`lineGroupId` diff --git a/docs/spec/tts/remote-tts.md b/docs/spec/tts/remote-tts.md index aca9d7175c..97cc40aa10 100644 --- a/docs/spec/tts/remote-tts.md +++ b/docs/spec/tts/remote-tts.md @@ -97,8 +97,14 @@ iOS 向けの選択はアナウンスには効かない。将来 iOS で内蔵 T Google TTS の日本語ローカル音声のように `QUALITY_NORMAL` へ横並びになる端末では 優劣を判定できない。判断材料が尽きると識別子のアルファベット順で決まってしまい、 品質と無関係に `ja-jp-x-htm-local` が常に選ばれ、ユーザーが端末設定で選んだ音声も -無視される。これを避けるため `patches/expo-speech+57.0.1.patch` で -`android.speech.tts.Voice` の生の情報を JS へ渡している。 +無視される。これを避ける判断材料として、JS 側は `android.speech.tts.Voice` の +生の情報を表す次のフィールドを扱える。 + +ただし現在の `expo-speech` はこれらを返さない。以前は +`patches/expo-speech+57.0.1.patch` で返すようにしていたが、`expo-speech` は Android で +ビルド済みの AAR(`local-maven-repo`)から取り込まれるため、`node_modules` のソースへの +パッチはビルドに入っておらず、本番のバイナリにも含まれていなかった。現在のパッチには +含めていない。 | フィールド | 由来 | 用途 | | --- | --- | --- | @@ -126,10 +132,8 @@ Google TTS の日本語ローカル音声のように `QUALITY_NORMAL` へ横並 ログへ残す。端末ごとに音声のラインナップが違い、音質の問い合わせは実機でどの音声が 選ばれたかが分からないと切り分けられないため。 -生メタデータの露出は `expo-speech` へのネイティブ patch なので、反映には -ネイティブビルドが必要で OTA アップデートでは配信されない。patch が当たっていない -バイナリでは各フィールドが `undefined` となり、従来どおり識別子順のタイブレークへ -フォールバックする。 +フィールドが返らない現在は、Android でも各フィールドが `undefined` となり、 +地域一致の次は識別子ベースの判定と識別子順のタイブレークで決まる。 ## テキストの前処理 diff --git a/ios/TrainLCD.xcodeproj/project.pbxproj b/ios/TrainLCD.xcodeproj/project.pbxproj index aebdd0c0d8..7370be417c 100644 --- a/ios/TrainLCD.xcodeproj/project.pbxproj +++ b/ios/TrainLCD.xcodeproj/project.pbxproj @@ -2562,7 +2562,7 @@ CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist; @@ -2602,7 +2602,7 @@ CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TrainLCD; @@ -2661,7 +2661,7 @@ CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -2718,7 +2718,7 @@ "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", "\"$(inherited)\"", ); - MARKETING_VERSION = 10.18.0; + MARKETING_VERSION = 10.18.1; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; @@ -2768,7 +2768,7 @@ CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2821,7 +2821,7 @@ "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", "\"$(inherited)\"", ); - MARKETING_VERSION = 10.18.0; + MARKETING_VERSION = 10.18.1; MTL_ENABLE_DEBUG_INFO = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_CPLUSPLUSFLAGS = "$(inherited)"; @@ -2849,7 +2849,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist; @@ -2889,7 +2889,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist; @@ -3101,7 +3101,7 @@ CODE_SIGN_ENTITLEMENTS = RideSessionActivity/CanaryRideSessionActivity.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3153,7 +3153,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3204,7 +3204,7 @@ CODE_SIGN_ENTITLEMENTS = WatchWidget/ProdWatchWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3262,7 +3262,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3313,7 +3313,7 @@ CODE_SIGN_ENTITLEMENTS = WatchWidget/CanaryWatchWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3370,7 +3370,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3419,7 +3419,7 @@ CODE_SIGN_ENTITLEMENTS = RideSessionActivity/ProdRideSessionActivity.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3471,7 +3471,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3690,7 +3690,7 @@ CODE_SIGN_ENTITLEMENTS = ProdAppClip/ProdAppClip.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3746,7 +3746,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3796,7 +3796,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryAppClip/CanaryAppClip.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3820,7 +3820,7 @@ "@executable_path/Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 10.18.0; + MARKETING_VERSION = 10.18.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3854,7 +3854,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2948; + CURRENT_PROJECT_VERSION = 2955; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3874,7 +3874,7 @@ "@executable_path/Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 10.18.0; + MARKETING_VERSION = 10.18.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PODS_ROOT = "${SRCROOT}/Pods"; diff --git a/package-lock.json b/package-lock.json index 1f71731c5b..0fb413a988 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trainlcd", - "version": "10.18.0", + "version": "10.18.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trainlcd", - "version": "10.18.0", + "version": "10.18.1", "hasInstallScript": true, "dependencies": { "@expo-google-fonts/roboto": "^0.2.3", @@ -2945,9 +2945,9 @@ } }, "node_modules/@fastify/busboy": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", - "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.2.tgz", + "integrity": "sha512-yXSS27qPExaXeuLvMRMXOLtpipzfQYNjG3FkunDWKGfMYjKuhFXko9CVzqxm8jcF+lmtS9Fd89QNdh9XDjnbNg==", "dev": true, "license": "MIT" }, @@ -10633,9 +10633,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "funding": [ { "type": "github", diff --git a/package.json b/package.json index c098d0d039..4579133110 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,13 @@ "react-native-watch-connectivity" ] } + }, + "autolinking": { + "android": { + "buildFromSource": [ + "expo-speech" + ] + } } }, "jest": { @@ -178,5 +185,5 @@ } }, "name": "trainlcd", - "version": "10.18.0" + "version": "10.18.1" } diff --git a/patches/expo-speech+57.0.1.patch b/patches/expo-speech+57.0.1.patch index 94d4c1a965..db9c3d70b0 100644 --- a/patches/expo-speech+57.0.1.patch +++ b/patches/expo-speech+57.0.1.patch @@ -1,157 +1,239 @@ diff --git a/node_modules/expo-speech/android/src/main/java/expo/modules/speech/SpeechModule.kt b/node_modules/expo-speech/android/src/main/java/expo/modules/speech/SpeechModule.kt -index 9ca869c..077cb21 100644 +index 9ca869c..d85cdc3 100644 --- a/node_modules/expo-speech/android/src/main/java/expo/modules/speech/SpeechModule.kt +++ b/node_modules/expo-speech/android/src/main/java/expo/modules/speech/SpeechModule.kt -@@ -4,6 +4,7 @@ import android.os.Bundle - import android.speech.tts.TextToSpeech - import android.speech.tts.UtteranceProgressListener - import android.speech.tts.Voice -+import android.util.Log - import expo.modules.kotlin.Promise +@@ -8,6 +8,7 @@ import expo.modules.kotlin.Promise import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition -@@ -17,6 +18,9 @@ const val speakingDoneEvent = "Exponent.speakingDone" - const val speakingStoppedEvent = "Exponent.speakingStopped" - const val speakingErrorEvent = "Exponent.speakingError" + import java.util.ArrayDeque ++import kotlinx.coroutines.launch + import java.util.Locale + import java.util.Queue -+// TrainLCD patch: voice / language 解決の診断ログ用タグ。 -+const val PATCH_LOG_TAG = "TrainLCDSpeech" -+ - class SpeechModule : Module() { - private val delayedUtterances: Queue = ArrayDeque() - private val delayedGetVoices: Queue = ArrayDeque() -@@ -83,6 +87,14 @@ class SpeechModule : Module() { - throw SpeechUnableToGetVoicesException(err) +@@ -36,12 +37,34 @@ class SpeechModule : Module() { + TextToSpeech.getMaxSpeechInputLength() } -+ // TrainLCD patch: 端末設定で選ばれているエンジン既定音声を JS 側へ伝える。 -+ // 一部エンジンは defaultVoice の取得で例外を投げるため防御する。 -+ val defaultVoiceName = try { -+ textToSpeech.defaultVoice?.name -+ } catch (_: Exception) { -+ null -+ } -+ - return nativeVoices.map { - val quality = if (it.quality > Voice.QUALITY_NORMAL) { - VoiceQuality.ENHANCED -@@ -90,11 +102,28 @@ class SpeechModule : Module() { - VoiceQuality.DEFAULT - } - -+ // TrainLCD patch: features / isNetworkConnectionRequired はエンジン実装に -+ // よっては例外を投げうるため、取得できない場合は保守的な既定値へ倒す。 -+ val networkRequired = try { -+ it.isNetworkConnectionRequired -+ } catch (_: Exception) { -+ false ++ // TrainLCD patch: 元の実装は shutdown したインスタンスを使い続けるため、Android の ++ // 「最近使ったアプリ」の一覧からアプリを消したあと、キャッシュに残ったプロセスで ++ // 開き直すと、発話がすべて "not bound to TTS engine" で失敗していた。破棄したら ++ // 参照を捨て、次に Activity が前面へ戻ってから作り直す。 ++ // JS 側の画面のアンマウントはこの通知より後に非同期で走るため、その間に届いた ++ // speak でエンジンを作り直すと、消したアプリが読み上げてしまう。前面へ戻るまでは ++ // 発話を受け付けない(expo-modules-core の AppContext.hostWasDestroyed と同じ区切り)。 ++ // TTS の状態は AsyncFunction と同じ modulesQueue のスレッドだけで扱い、ロックを使わない。 + OnActivityDestroys { +- textToSpeech.shutdown() ++ appContext.modulesQueue.launch { ++ hostDestroyed = true ++ _textToSpeech?.shutdown() ++ _textToSpeech = null ++ _ttsReady = false ++ stopDelayedUtterances() ++ resolveDelayedGetVoicesWithEmptyList() + } -+ val notInstalled = try { -+ it.features?.contains(TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED) == true -+ } catch (_: Exception) { -+ false + } + ++ OnActivityEntersForeground { ++ appContext.modulesQueue.launch { ++ hostDestroyed = false + } ++ } + - VoiceRecord( - identifier = it.name, - name = it.name, - quality = quality, -- language = LanguageUtils.getISOCode(it.locale) -+ language = LanguageUtils.getISOCode(it.locale), -+ qualityScore = it.quality, -+ isDefault = defaultVoiceName != null && it.name == defaultVoiceName, -+ networkRequired = networkRequired, -+ notInstalled = notInstalled - ) ++ // TrainLCD patch: isSpeaking / stop のためだけにエンジンへ接続し直さない + AsyncFunction("isSpeaking") { +- textToSpeech.isSpeaking ++ _textToSpeech?.isSpeaking ?: false + } + + AsyncFunction("getVoices") { promise: Promise -> +@@ -56,7 +79,8 @@ class SpeechModule : Module() { + } + + AsyncFunction("stop") { +- textToSpeech.stop() ++ _textToSpeech?.stop() ++ Unit } - } -@@ -103,24 +132,48 @@ class SpeechModule : Module() { - options.pitch?.let(textToSpeech::setPitch) - options.rate?.let(textToSpeech::setSpeechRate) -- textToSpeech.language = options.language?.let { -- val locale = Locale(it) -- val languageAvailable = textToSpeech.isLanguageAvailable(locale) + AsyncFunction("speak") { id: String, text: String, options: SpeechOptions -> +@@ -64,7 +88,10 @@ class SpeechModule : Module() { + throw SpeechInputIsToLongException() + } + +- if (isTextToSpeechReady) { ++ if (hostDestroyed) { ++ // 受け付けなかったことを JS へ返し、完了待ちのまま残さない ++ sendEvent(speakingStoppedEvent, idToMap(id)) ++ } else if (isTextToSpeechReady) { + speakOut(id, text, options) + } else { + delayedUtterances.add(Utterance(id, text, options)) +@@ -143,50 +170,114 @@ class SpeechModule : Module() { + private val isTextToSpeechReady + get() = _ttsReady + +- private val textToSpeech: TextToSpeech by lazy { +- val newTtsInstance = TextToSpeech(appContext.reactContext) { status: Int -> +- if (status == TextToSpeech.SUCCESS) { +- // synchronize because in some cases this runs on another thread and _textToSpeech is null +- synchronized(this@SpeechModule) { +- _ttsReady = true +- _textToSpeech!!.setOnUtteranceProgressListener(object : UtteranceProgressListener() { - -- return@let if ( -- languageAvailable != TextToSpeech.LANG_MISSING_DATA && -- languageAvailable != TextToSpeech.LANG_NOT_SUPPORTED -- ) { -- locale -- } else { -- Locale.getDefault() -+ // TrainLCD patch: voice 指定がある場合は voice を最優先で適用する。 -+ // Voice は自身の Locale を持つため setLanguage は不要で、呼ぶとエンジンに -+ // よっては直後の setVoice と競合して直前の音声(別言語)のまま合成される -+ // ことがある。voice が見つからない場合のみ language から解決する。 -+ // 一部エンジンで getVoices が発話中に例外を投げることがあるため防御する。 -+ val matchedVoice = options.voice?.let { voiceName -> -+ try { -+ textToSpeech.voices?.firstOrNull { it.name == voiceName } -+ } catch (_: Exception) { -+ null +- override fun onStart(utteranceId: String) { +- sendEvent(speakingStartedEvent, idToMap(utteranceId)) +- } +- +- override fun onRangeStart(utteranceId: String, start: Int, end: Int, frame: Int) { +- val map = Bundle().apply { +- putString("id", utteranceId) +- putInt("charIndex", start) +- putInt("charLength", end - start) +- } +- sendEvent(speakingWillSayNextStringEvent, map) +- } +- +- override fun onDone(utteranceId: String) { +- sendEvent(speakingDoneEvent, idToMap(utteranceId)) +- } +- +- override fun onStop(utteranceId: String, interrupted: Boolean) { +- sendEvent(speakingStoppedEvent, idToMap(utteranceId)) +- } +- +- override fun onError(utteranceId: String) { +- sendEvent(speakingErrorEvent, idToMap(utteranceId)) +- } +- }) +- for ((id, text, options) in delayedUtterances) { +- speakOut(id, text, options) +- } +- for (promise in delayedGetVoices) { +- promise.resolve(getVoices()) +- } +- } ++ // TrainLCD patch: by lazy では shutdown 後に作り直せないため、破棄されていれば作り直す。 ++ // modulesQueue のスレッドからだけ呼ぶ ++ private val textToSpeech: TextToSpeech ++ get() = _textToSpeech ?: createTextToSpeech() ++ ++ private fun createTextToSpeech(): TextToSpeech { ++ lateinit var newTtsInstance: TextToSpeech ++ newTtsInstance = TextToSpeech(appContext.reactContext) { status: Int -> ++ // TrainLCD patch: この通知は TextToSpeech が内部のロックを持ったまま呼ぶ(メインスレッド、 ++ // バインダーのスレッド、またはコンストラクタの中)。ここでは状態に触れず modulesQueue へ回す ++ appContext.modulesQueue.launch { ++ onTextToSpeechInit(newTtsInstance, status) } -- } ?: Locale.getDefault() + } + _textToSpeech = newTtsInstance +- newTtsInstance ++ return newTtsInstance ++ } ++ ++ private fun onTextToSpeechInit(tts: TextToSpeech, status: Int) { ++ // 初期化を待つ間に破棄された古いインスタンス。接続が後から確立するので、ここで切る ++ if (tts !== _textToSpeech) { ++ tts.shutdown() ++ return ++ } ++ if (status != TextToSpeech.SUCCESS) { ++ // 捨てておけば、次の呼び出しで作り直す ++ tts.shutdown() ++ _textToSpeech = null ++ _ttsReady = false ++ failDelayedRequests() ++ return + } + -+ if (matchedVoice != null) { -+ textToSpeech.setVoice(matchedVoice) -+ } else { -+ // TrainLCD patch: Locale(String) は 'en-US' のような BCP 47 タグ全体を -+ // language として扱う不正な Locale を生成し、isLanguageAvailable が常に -+ // 失敗して Locale.getDefault()(端末既定言語)で合成されてしまう。 -+ // forLanguageTag で正しく解釈し、地域付きタグの音声データが無い端末では -+ // 言語のみの Locale へ段階的にフォールバックする。 -+ val resolvedLocale = options.language?.let { -+ val exactLocale = Locale.forLanguageTag(it.replace('_', '-')) -+ val languageOnlyLocale = Locale(exactLocale.language) ++ _ttsReady = true ++ tts.setOnUtteranceProgressListener(object : UtteranceProgressListener() { + -+ val isUsable = { locale: Locale -> -+ val availability = textToSpeech.isLanguageAvailable(locale) -+ availability != TextToSpeech.LANG_MISSING_DATA && -+ availability != TextToSpeech.LANG_NOT_SUPPORTED ++ override fun onStart(utteranceId: String) { ++ sendEvent(speakingStartedEvent, idToMap(utteranceId)) ++ } ++ ++ override fun onRangeStart(utteranceId: String, start: Int, end: Int, frame: Int) { ++ val map = Bundle().apply { ++ putString("id", utteranceId) ++ putInt("charIndex", start) ++ putInt("charLength", end - start) + } ++ sendEvent(speakingWillSayNextStringEvent, map) ++ } ++ ++ override fun onDone(utteranceId: String) { ++ sendEvent(speakingDoneEvent, idToMap(utteranceId)) ++ } ++ ++ override fun onStop(utteranceId: String, interrupted: Boolean) { ++ sendEvent(speakingStoppedEvent, idToMap(utteranceId)) ++ } ++ ++ override fun onError(utteranceId: String) { ++ sendEvent(speakingErrorEvent, idToMap(utteranceId)) ++ } ++ }) ++ // 作り直したときに前の発話を流し直さないよう、取り出しながら消化する ++ if (hostDestroyed) { ++ stopDelayedUtterances() ++ } ++ // この消化は modulesQueue.launch の中で動き、例外を受け止める仕組みが無いとプロセスごと ++ // 落ちる。1 件ずつ受け止め、残りの要求を待たせたままにしない ++ while (true) { ++ val (id, text, options) = delayedUtterances.poll() ?: break ++ try { ++ speakOut(id, text, options) ++ } catch (e: Exception) { ++ sendEvent(speakingErrorEvent, idToMap(id)) ++ } ++ } ++ while (true) { ++ val promise = delayedGetVoices.poll() ?: break ++ try { ++ promise.resolve(getVoices()) ++ } catch (e: SpeechUnableToGetVoicesException) { ++ promise.reject(e) ++ } ++ } ++ } ++ ++ // 読み上げずに捨てる発話。JS 側が完了を待ち続けないよう停止として通知する ++ private fun stopDelayedUtterances() { ++ while (true) { ++ val utterance = delayedUtterances.poll() ?: break ++ sendEvent(speakingStoppedEvent, idToMap(utterance.id)) ++ } ++ } ++ ++ // 初期化に失敗した回の要求を待たせたままにしない。上流 58.0.0 の #49228 と同じく、 ++ // 発話はエラーとして通知し、音声一覧は空で返す ++ private fun failDelayedRequests() { ++ while (true) { ++ val utterance = delayedUtterances.poll() ?: break ++ sendEvent(speakingErrorEvent, idToMap(utterance.id)) ++ } ++ resolveDelayedGetVoicesWithEmptyList() ++ } ++ ++ private fun resolveDelayedGetVoicesWithEmptyList() { ++ while (true) { ++ val promise = delayedGetVoices.poll() ?: break ++ promise.resolve(emptyList()) ++ } + } -- options.voice?.let { voiceName -> -- textToSpeech.voices -- .firstOrNull { it.name == voiceName } -- ?.let(textToSpeech::setVoice) -+ when { -+ isUsable(exactLocale) -> exactLocale -+ isUsable(languageOnlyLocale) -> languageOnlyLocale -+ else -> Locale.getDefault() -+ } -+ } ?: Locale.getDefault() -+ val languageResult = textToSpeech.setLanguage(resolvedLocale) -+ Log.d( -+ PATCH_LOG_TAG, -+ "speak id=$id engine=${textToSpeech.defaultEngine} requestedVoice=${options.voice} requestedLanguage=${options.language} setLanguage=$resolvedLocale result=$languageResult" -+ ) - } + // Helpers +@@ -200,7 +291,10 @@ class SpeechModule : Module() { + val options: SpeechOptions + ) - val params = Bundle().apply { -diff --git a/node_modules/expo-speech/android/src/main/java/expo/modules/speech/VoiceRecord.kt b/node_modules/expo-speech/android/src/main/java/expo/modules/speech/VoiceRecord.kt -index 480ce46..e85ad12 100644 ---- a/node_modules/expo-speech/android/src/main/java/expo/modules/speech/VoiceRecord.kt -+++ b/node_modules/expo-speech/android/src/main/java/expo/modules/speech/VoiceRecord.kt -@@ -15,5 +15,19 @@ data class VoiceRecord( - @Field val identifier: String, - @Field val name: String, - @Field val quality: VoiceQuality, -- @Field val language: String -+ @Field val language: String, -+ // TrainLCD patch: 音声選択の精度を上げるための Android 固有メタデータ。 -+ // quality は ENHANCED / DEFAULT の 2 値へ丸められており、ローカル音声同士の -+ // 優劣を判定できない(Google TTS の日本語ローカル音声は軒並み QUALITY_NORMAL -+ // に並ぶ)。android.speech.tts.Voice が持つ生の情報を併せて渡すことで、 -+ // JS 側が識別子のアルファベット順という無根拠なタイブレークに頼らずに済む。 -+ // -+ // - qualityScore: Voice.getQuality() の生値 (QUALITY_VERY_LOW=100 〜 VERY_HIGH=500) -+ // - isDefault: 端末設定で選ばれているエンジン既定音声か -+ // - networkRequired: 合成にネットワーク接続が必要か(識別子の 'network' 判定より正確) -+ // - notInstalled: 音声データが未ダウンロードで、指定しても合成できない状態か -+ @Field val qualityScore: Int, -+ @Field val isDefault: Boolean, -+ @Field val networkRequired: Boolean, -+ @Field val notInstalled: Boolean - ) : Record +- // do not refer to these - they're only needed when initializing `textToSpeech` ++ // TrainLCD patch: 以下の状態と待ち行列は modulesQueue のスレッドだけで読み書きする + private var _textToSpeech: TextToSpeech? = null + private var _ttsReady = false ++ ++ // TrainLCD patch: Activity が破棄されてから次に前面へ戻るまでの間か ++ private var hostDestroyed = false + } diff --git a/patches/expo-task-manager+57.0.13.patch b/patches/expo-task-manager+57.0.13.patch deleted file mode 100644 index c753e71731..0000000000 --- a/patches/expo-task-manager+57.0.13.patch +++ /dev/null @@ -1,29 +0,0 @@ -diff --git a/node_modules/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java b/node_modules/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java -index 82bc0db..905c385 100644 ---- a/node_modules/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java -+++ b/node_modules/expo-task-manager/android/src/main/java/expo/modules/taskManager/TaskManagerUtils.java -@@ -214,8 +214,22 @@ public class TaskManagerUtils implements TaskManagerUtilsInterface { - jobBuilder.setImportantWhileForeground(true) - .setOverrideDeadline(DEFAULT_OVERRIDE_DEADLINE); - } else { -- // For Android 12 (API 31) and above -- jobBuilder.setExpedited(true); -+ // For Android 12 (API 31) and above. -+ // setExpedited(true) alone causes JobInfo.Builder.build() to throw -+ // IllegalArgumentException("You're trying to build a job with no constraints, -+ // this is not allowed.") on Android 14+/16 because expedited jobs do not -+ // register an early/late/functional constraint, and the build-time validator -+ // counts none of setPersisted/setRequiresDeviceIdle(false) as a constraint. -+ // We apply the minimum-latency + override-deadline fallback uniformly across -+ // API 31+ rather than gating on SDK_INT >= 36, because OEM/patch-level -+ // variance on Android 14 and 15 can still trip the same crash. -+ // Note on the trade-off: the JS-side watchPositionAsync bypass is gated by -+ // NEEDS_JOBSCHEDULER_BYPASS (src/constants/native.ts), which is true only on -+ // API 36+, so it does NOT offset the loss of expedited priority on API 31-35. -+ // There, the Android foreground location service keeping the process alive is -+ // the only mitigation, at the cost of a possible JS delivery delay under Doze. -+ jobBuilder.setMinimumLatency(0) -+ .setOverrideDeadline(DEFAULT_OVERRIDE_DEADLINE); - } - - return jobBuilder.build(); diff --git a/src/components/RouteInfoModal.test.tsx b/src/components/RouteInfoModal.test.tsx new file mode 100644 index 0000000000..85772fac5d --- /dev/null +++ b/src/components/RouteInfoModal.test.tsx @@ -0,0 +1,174 @@ +import { act, render } from '@testing-library/react-native'; +import { useAtomValue } from 'jotai'; +import type React from 'react'; +import type { Station } from '~/@types/graphql'; +import { LIGHT_APP_COLORS } from '~/constants/colorScheme'; +import { appColorsAtom } from '~/store/atoms/colorScheme'; +import { isLEDThemeAtom } from '~/store/atoms/theme'; +import { RouteInfoModal } from './RouteInfoModal'; + +jest.mock('jotai', () => ({ + ...jest.requireActual('jotai'), + useAtomValue: jest.fn(), +})); + +// 一覧の初回レイアウト(onLoad)とスクロールの呼び出しだけを確かめたいので、 +// FlashList は ref に scrollToIndex を生やし onLoad を受け取るだけの入れ物にする +const mockScrollToIndex = jest.fn(); +const mockFlashListOnLoad: { current: (() => void) | null } = { + current: null, +}; +jest.mock('@shopify/flash-list', () => { + const ReactModule = require('react'); + return { + FlashList: ReactModule.forwardRef( + (props: { onLoad?: () => void }, ref: React.Ref) => { + ReactModule.useImperativeHandle(ref, () => ({ + scrollToIndex: mockScrollToIndex, + })); + mockFlashListOnLoad.current = props.onLoad ?? null; + return null; + } + ), + }; +}); + +// 実物は閉じると中身をアンマウントするので、それに合わせる +jest.mock('./CustomModal', () => ({ + CustomModal: (props: { visible: boolean; children?: React.ReactNode }) => + props.visible ? (props.children ?? null) : null, +})); + +jest.mock('~/translation', () => ({ + isJapanese: true, + translate: jest.fn((key: string) => key), +})); + +const stations = [1, 2, 3, 4, 5].map( + (id) => + ({ + id, + groupId: id, + name: `駅${id}`, + nameRoman: `Station ${id}`, + line: { id: 10 }, + lines: [{ id: 10 }], + }) as unknown as Station +); + +const renderModal = (props: { + visible: boolean; + currentStation: Station | null; + stations?: Station[]; +}) => ( + +); + +describe('RouteInfoModal', () => { + beforeEach(() => { + (useAtomValue as jest.Mock).mockImplementation((atom: unknown) => { + if (atom === appColorsAtom) return LIGHT_APP_COLORS; + if (atom === isLEDThemeAtom) return false; + return null; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + mockFlashListOnLoad.current = null; + }); + + it('一覧の初回レイアウト後に現在の最寄り駅までヘッダーの高さだけずらしてアニメーションでスクロールする', () => { + render(renderModal({ visible: true, currentStation: stations[3] })); + expect(mockScrollToIndex).not.toHaveBeenCalled(); + + act(() => mockFlashListOnLoad.current?.()); + + expect(mockScrollToIndex).toHaveBeenCalledTimes(1); + const [params] = mockScrollToIndex.mock.calls[0]; + expect(params.index).toBe(3); + expect(params.animated).toBe(true); + expect(params.viewOffset).toBeLessThan(0); + }); + + it('最寄り駅が先頭の駅ならスクロールしない', () => { + render(renderModal({ visible: true, currentStation: stations[0] })); + act(() => mockFlashListOnLoad.current?.()); + + expect(mockScrollToIndex).not.toHaveBeenCalled(); + }); + + it('最寄り駅が一覧に無ければスクロールしない', () => { + render( + renderModal({ + visible: true, + currentStation: { id: 99, groupId: 99 } as Station, + }) + ); + act(() => mockFlashListOnLoad.current?.()); + + expect(mockScrollToIndex).not.toHaveBeenCalled(); + }); + + it('最寄り駅の id・groupId が null でも、同じく null の別の駅の行へスクロールしない', () => { + const nullIdStation = { + ...stations[0], + id: null, + groupId: null, + } as unknown as Station; + render( + renderModal({ + visible: true, + currentStation: { id: null, groupId: null } as unknown as Station, + stations: [...stations, nullIdStation], + }) + ); + act(() => mockFlashListOnLoad.current?.()); + + expect(mockScrollToIndex).not.toHaveBeenCalled(); + }); + + it('乗換駅の行が間引かれていても groupId で同じ駅の行へスクロールする', () => { + // 3 と 30 は同じ乗換駅。手前の区間の 3 は一覧から間引かれる + const transferStations = [ + stations[0], + stations[1], + stations[2], + { ...stations[2], id: 30 } as Station, + stations[3], + ]; + render( + renderModal({ + visible: true, + currentStation: stations[2], + stations: transferStations, + }) + ); + act(() => mockFlashListOnLoad.current?.()); + + expect(mockScrollToIndex.mock.calls[0][0].index).toBe(2); + }); + + it('開き直すたびにもう一度スクロールする', () => { + const { rerender } = render( + renderModal({ visible: true, currentStation: stations[3] }) + ); + act(() => mockFlashListOnLoad.current?.()); + expect(mockScrollToIndex).toHaveBeenCalledTimes(1); + + rerender(renderModal({ visible: false, currentStation: stations[3] })); + rerender(renderModal({ visible: true, currentStation: stations[3] })); + // 開き直した直後は前回の onLoad を引き継がず、新しい一覧のレイアウトを待つ + expect(mockScrollToIndex).toHaveBeenCalledTimes(1); + + act(() => mockFlashListOnLoad.current?.()); + expect(mockScrollToIndex).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/components/RouteInfoModal.tsx b/src/components/RouteInfoModal.tsx index 2d89d07c54..7a32dd0eea 100644 --- a/src/components/RouteInfoModal.tsx +++ b/src/components/RouteInfoModal.tsx @@ -1,7 +1,7 @@ -import { FlashList } from '@shopify/flash-list'; +import { FlashList, type FlashListRef } from '@shopify/flash-list'; import { BlurView } from 'expo-blur'; import { useAtomValue } from 'jotai'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Platform, StyleSheet, useWindowDimensions, View } from 'react-native'; import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; import type { Station, TrainType } from '~/@types/graphql'; @@ -100,6 +100,8 @@ type Props = { wantedDestinationGroupId?: number | null; /** 終着駅の設定トグル */ onToggleDestination?: (station: Station) => void; + /** 開いたときにスクロールして見せる駅(現在の最寄り駅) */ + currentStation?: Station | null; }; export const RouteInfoModal = ({ @@ -113,6 +115,7 @@ export const RouteInfoModal = ({ onToggleNotification, wantedDestinationGroupId, onToggleDestination, + currentStation, }: Props) => { const isLEDTheme = useAtomValue(isLEDThemeAtom); const colors = useAtomValue(appColorsAtom); @@ -253,6 +256,51 @@ export const RouteInfoModal = ({ [stations] ); + const currentStationIndex = useMemo(() => { + if (!currentStation) return -1; + // 乗換駅は前後どちらかの区間の行が間引かれているので、ID で見つからなければ groupId で探す + // id・groupId は null になりうる。null どうしを同じ駅とみなさない + const byId = + currentStation.id != null + ? deduppedStations.findIndex((s) => s.id === currentStation.id) + : -1; + if (byId !== -1 || currentStation.groupId == null) return byId; + return deduppedStations.findIndex( + (s) => s.groupId === currentStation.groupId + ); + }, [currentStation, deduppedStations]); + + const listRef = useRef>(null); + // CustomModal は閉じると中身をアンマウントするので、開くたびに一覧の初回レイアウトを待ってからスクロールする + const [isListLoaded, setIsListLoaded] = useState(false); + const hasScrolledToCurrentRef = useRef(false); + const headerHeightRef = useRef(headerHeight); + headerHeightRef.current = headerHeight; + + useEffect(() => { + if (!visible) { + setIsListLoaded(false); + hasScrolledToCurrentRef.current = false; + } + }, [visible]); + + useEffect(() => { + if (!visible || !isListLoaded || hasScrolledToCurrentRef.current) return; + if (currentStationIndex <= 0) return; + hasScrolledToCurrentRef.current = true; + // 一覧の上端はヘッダーの裏に潜るので、ヘッダーの高さだけずらしてヘッダーの直下に出す。 + // 末尾付近の駅は FlashList が最大スクロール量で止める。 + // どこまで進んだかが目で追えるようアニメーションさせる。遠い駅では FlashList が + // 手前(画面2枚分)まで一気に飛んでから残りをアニメーションする + listRef.current?.scrollToIndex({ + index: currentStationIndex, + animated: true, + viewOffset: -headerHeightRef.current, + }); + }, [visible, isListLoaded, currentStationIndex]); + + const handleListLoad = useCallback(() => setIsListLoaded(true), []); + const dynamicMinHeight = useMemo(() => { const count = deduppedStations.length; const content = headerHeight + count * 80 + Math.max(0, count - 1) * 8 + 72; @@ -315,7 +363,9 @@ export const RouteInfoModal = ({ + ref={listRef} data={deduppedStations} + onLoad={handleListLoad} renderItem={renderItem} keyExtractor={keyExtractor} ItemSeparatorComponent={EmptyLineSeparator} diff --git a/src/components/SelectBoundModal.render.test.tsx b/src/components/SelectBoundModal.render.test.tsx index 3f61a7292a..4f500d3f81 100644 --- a/src/components/SelectBoundModal.render.test.tsx +++ b/src/components/SelectBoundModal.render.test.tsx @@ -542,6 +542,106 @@ describe('SelectBoundModal', () => { expect(props.stations.map((station) => station.groupId)).toEqual([1, 2, 3]); }); + describe('停車駅一覧を開いたときに見せる駅', () => { + const getLastCurrentStation = () => { + const lastCall = + mockRouteInfoModal.mock.calls[mockRouteInfoModal.mock.calls.length - 1]; + return (lastCall?.[0] as { currentStation: { id: number } | null }) + .currentStation; + }; + const renderModal = () => + render( + + ); + const routeStations = [1, 2, 3, 4, 5].map((id) => ({ + id, + groupId: id, + line: { id: 10 }, + lines: [{ id: 10 }], + })); + const getIsPassMock = jest.requireMock('~/utils/isPass') + .default as jest.Mock; + + afterEach(() => { + getIsPassMock.mockImplementation(() => false); + }); + + it('最寄り駅に停車するならその駅を渡す', () => { + mockAtomValues({ + pendingStation: { id: 3, groupId: 3, lines: [{ id: 10 }] }, + pendingStations: routeStations, + pendingLine: { id: 10, name: '山手線', nameRoman: 'Yamanote Line' }, + }); + + renderModal(); + + expect(getLastCurrentStation()?.id).toBe(3); + }); + + it('最寄り駅を通過するなら経路上で最も近い停車駅を渡す', () => { + // 2・3・5 を通過。3 からは 4 が最も近い + getIsPassMock.mockImplementation((s?: { id: number }) => + [2, 3, 5].includes(s?.id ?? -1) + ); + mockAtomValues({ + pendingStation: { id: 3, groupId: 3, lines: [{ id: 10 }] }, + pendingStations: routeStations, + pendingLine: { id: 10, name: '山手線', nameRoman: 'Yamanote Line' }, + }); + + renderModal(); + + expect(getLastCurrentStation()?.id).toBe(4); + }); + + it('前後の停車駅が等距離なら手前の駅を渡す', () => { + getIsPassMock.mockImplementation((s?: { id: number }) => s?.id === 3); + mockAtomValues({ + pendingStation: { id: 3, groupId: 3, lines: [{ id: 10 }] }, + pendingStations: routeStations, + pendingLine: { id: 10, name: '山手線', nameRoman: 'Yamanote Line' }, + }); + + renderModal(); + + expect(getLastCurrentStation()?.id).toBe(2); + }); + + it('最寄り駅が経路外なら何も渡さない', () => { + mockAtomValues({ + pendingStation: { id: 99, groupId: 99, lines: [{ id: 10 }] }, + pendingStations: routeStations, + pendingLine: { id: 10, name: '山手線', nameRoman: 'Yamanote Line' }, + }); + + renderModal(); + + expect(getLastCurrentStation()).toBeNull(); + }); + + it('最寄り駅の id・groupId が null でも、同じく null の別の駅を最寄り駅とみなさない', () => { + mockAtomValues({ + pendingStation: { id: null, groupId: null, lines: [{ id: 10 }] }, + pendingStations: [ + ...routeStations, + { id: null, groupId: null, line: { id: 10 }, lines: [{ id: 10 }] }, + ], + pendingLine: { id: 10, name: '山手線', nameRoman: 'Yamanote Line' }, + }); + + renderModal(); + + expect(getLastCurrentStation()).toBeNull(); + }); + }); + // 乗車駅が未設定だと effectiveStation は stations[0] へ倒れるため、 // そこから向きを決めると常に INBOUND になってしまう it('乗車駅が未設定の場合はGPS確定駅の座標から保存する向きを決める', async () => { diff --git a/src/components/SelectBoundModal.tsx b/src/components/SelectBoundModal.tsx index 5a382041cb..a319e872da 100644 --- a/src/components/SelectBoundModal.tsx +++ b/src/components/SelectBoundModal.tsx @@ -910,6 +910,28 @@ export const SelectBoundModal: React.FC = ({ [stations] ); + // 停車駅一覧を開いたときに見せる駅。最寄り駅を通過する種別では一覧に出ないので、 + // 経路上で最も近い停車駅で代える(等距離なら手前の駅) + const routeInfoCurrentStation = useMemo(() => { + if (!station) return null; + // id・groupId は null になりうる。null どうしを同じ駅とみなさない + const byId = + station.id != null ? stations.findIndex((s) => s.id === station.id) : -1; + const index = + byId !== -1 || station.groupId == null + ? byId + : stations.findIndex((s) => s.groupId === station.groupId); + if (index === -1) return null; + if (!getIsPass(stations[index])) return stations[index]; + for (let distance = 1; distance < stations.length; distance++) { + const before = stations[index - distance]; + if (before && !getIsPass(before)) return before; + const after = stations[index + distance]; + if (after && !getIsPass(after)) return after; + } + return null; + }, [station, stations]); + const isBus = isBusLine(line); const trainTypeModalLine = useMemo(() => { @@ -1106,6 +1128,7 @@ export const SelectBoundModal: React.FC = ({ onToggleNotification={handleToggleNotification} wantedDestinationGroupId={wantedDestination?.groupId ?? null} onToggleDestination={handleToggleDestination} + currentStation={routeInfoCurrentStation} /> ({ + useLoopLine: jest.fn(), +})); + +const mockUseLoopLine = useLoopLine as jest.MockedFunction; + +// 東海道新幹線は東京起点の並び。三島→新横浜は上り(OUTBOUNDで逆順に辿る)。 +const shinYokohama = createStation(100203, { + latitude: 35.5069, + longitude: 139.6175, +}); +const odawara = createStation(100204, { + latitude: 35.2564, + longitude: 139.1552, + stopCondition: StopCondition.Not, +}); +const atami = createStation(100205, { + latitude: 35.1036, + longitude: 139.0779, + stopCondition: StopCondition.Not, +}); +const mishima = createStation(100206, { + latitude: 35.1265, + longitude: 138.9108, +}); + +const setup = ( + direction: 'INBOUND' | 'OUTBOUND', + isLoopLine = false +): ReturnType => { + mockUseLoopLine.mockReturnValue({ + isLoopLine, + } as ReturnType); + const store = createStore(); + store.set(stationsAtom, [shinYokohama, odawara, atami, mishima]); + store.set(selectedDirectionAtom, direction); + store.set(stationAtom, mishima); + store.set(locationAtom, { + timestamp: 0, + coords: { + latitude: 35.40113, + longitude: 139.39607, + accuracy: 7, + altitude: null, + altitudeAccuracy: null, + heading: null, + speed: 80, + }, + }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + return renderHook(() => usePassedStation(), { wrapper }).result.current; +}; + +describe('usePassedStation', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('進行方向順に並べ直して、通り過ぎた通過駅を返す', () => { + expect(setup('OUTBOUND')?.id).toBe(odawara.id); + }); + + it('逆方向に乗っている扱いなら現在駅の先に区間が無く、何も返さない', () => { + expect(setup('INBOUND')).toBeUndefined(); + }); + + it('ループ線では何も返さない', () => { + expect(setup('OUTBOUND', true)).toBeUndefined(); + }); +}); diff --git a/src/hooks/usePassedStation.ts b/src/hooks/usePassedStation.ts new file mode 100644 index 0000000000..3d3d03000f --- /dev/null +++ b/src/hooks/usePassedStation.ts @@ -0,0 +1,59 @@ +import { useAtomValue } from 'jotai'; +import { useMemo } from 'react'; +import type { Station } from '~/@types/graphql'; +import { locationAtom } from '~/store/atoms/location'; +import dropEitherJunctionStation from '~/utils/dropJunctionStation'; +import { findPassedStation } from '~/utils/passedStation'; +import reverseStations from '~/utils/reverseStations'; +import { + selectedDirectionAtom, + stationAtom, + stationsAtom, +} from '../store/atoms/station'; +import { useLoopLine } from './useLoopLine'; + +/** + * 到着圏を取りこぼした通過駅のうち、現在地がすでに通り過ぎたものを返す。 + * useRefreshStation が現在駅を後方に取り残さないための自己修復に使う。 + * + * ループ線は対象外にする。進行方向順の並びが終端で折り返すため、 + * 区間の選び方が非ループ線と同じにならない。 + */ +export const usePassedStation = (): Station | undefined => { + const location = useAtomValue(locationAtom); + const stationsFromState = useAtomValue(stationsAtom); + const selectedDirection = useAtomValue(selectedDirectionAtom); + const currentStation = useAtomValue(stationAtom); + const { isLoopLine } = useLoopLine(); + + const latitude = location?.coords.latitude; + const longitude = location?.coords.longitude; + const accuracy = location?.coords.accuracy; + + const orderedStations = useMemo(() => { + if (selectedDirection == null || isLoopLine) { + return null; + } + const stations = dropEitherJunctionStation( + stationsFromState, + selectedDirection + ); + return selectedDirection === 'INBOUND' + ? stations + : reverseStations(stations); + }, [isLoopLine, selectedDirection, stationsFromState]); + + return useMemo( + () => + orderedStations + ? findPassedStation( + orderedStations, + currentStation, + latitude, + longitude, + accuracy + ) + : undefined, + [accuracy, currentStation, latitude, longitude, orderedStations] + ); +}; diff --git a/src/hooks/useRefreshStation.test.tsx b/src/hooks/useRefreshStation.test.tsx index 10806dbecb..9543612293 100644 --- a/src/hooks/useRefreshStation.test.tsx +++ b/src/hooks/useRefreshStation.test.tsx @@ -6,6 +6,7 @@ import * as useApproachingStationModule from '~/hooks/useApproachingStation'; import * as useCanGoForwardModule from '~/hooks/useCanGoForward'; import * as useNearestStationModule from '~/hooks/useNearestStation'; import * as useNextStationModule from '~/hooks/useNextStation'; +import * as usePassedStationModule from '~/hooks/usePassedStation'; import { useRefreshStation } from '~/hooks/useRefreshStation'; import * as useThresholdModule from '~/hooks/useThreshold'; import * as useWrongDirectionDetectorModule from '~/hooks/useWrongDirectionDetector'; @@ -85,6 +86,9 @@ describe('useRefreshStation', () => { jest .spyOn(useApproachingStationModule, 'useApproachingStation') .mockReturnValue(mockStation); + jest + .spyOn(usePassedStationModule, 'usePassedStation') + .mockReturnValue(undefined); }); afterEach(() => { @@ -395,4 +399,80 @@ describe('useRefreshStation', () => { const { nextState } = setupEtaDwelling('AT_STATION'); expect(nextState.arrived).toBe(false); }); + + describe('通過駅の取りこぼしからの自己修復', () => { + const passedStation = { + ...mockStation, + id: 40, + groupId: 40, + latitude: 35.2, + longitude: 135.0, + stopCondition: StopCondition.Not, + }; + const stalePrevStation = { ...mockStation, id: 50, groupId: 50 }; + + const renderWithLocation = (latitude: number) => { + mockUseAtomValue + .mockReturnValueOnce({ + coords: { latitude, longitude: 135.0 }, + }) // locationAtom + .mockReturnValue({ targetStationIds: [] }); // notifyState + + const setStation = jest.fn(); + const setNavigation = jest.fn(); + mockUseSetAtom + .mockReturnValueOnce(setStation) + .mockReturnValueOnce(setNavigation) + .mockReturnValue(jest.fn()); + + jest + .spyOn(useNearestStationModule, 'useNearestStation') + .mockReturnValue(mockStation); + jest + .spyOn(useNextStationModule, 'useNextStation') + .mockReturnValue(mockStation); + jest + .spyOn(usePassedStationModule, 'usePassedStation') + .mockReturnValue(passedStation); + jest + .spyOn(useCanGoForwardModule, 'useCanGoForward') + .mockReturnValue(true); + jest.spyOn(useThresholdModule, 'useThreshold').mockReturnValue({ + arrivedThreshold: 100, + approachingThreshold: 300, + }); + jest + .spyOn(useWrongDirectionDetectorModule, 'useWrongDirectionDetector') + .mockReturnValue({ + isWrongDirection: false, + isLoopLineWrongDirection: false, + }); + + renderHook(() => useRefreshStation(), { + wrapper: ({ children }) => {children}, + }); + + expect(setStation).toHaveBeenCalled(); + const updater = setStation.mock.calls[0][0] as (prev: any) => any; + return { + nextState: updater({ station: stalePrevStation }), + setNavigation, + }; + }; + + it('到着していなければ、通り過ぎた通過駅へ現在駅を進める(到着は扱わない)', () => { + // 最寄り駅(35.0,135.0)から約55km北。到着圏の外 + const { nextState, setNavigation } = renderWithLocation(35.5); + expect(nextState.station).toBe(passedStation); + expect(nextState.arrived).toBe(false); + // ヘッダーの駅(停車駅)は動かさない + expect(setNavigation).not.toHaveBeenCalled(); + }); + + it('到着中は到着した最寄り駅が優先される', () => { + const { nextState } = renderWithLocation(35.0); + expect(nextState.station).toBe(mockStation); + expect(nextState.arrived).toBe(true); + }); + }); }); diff --git a/src/hooks/useRefreshStation.ts b/src/hooks/useRefreshStation.ts index a2c1b18d8d..57009a5e48 100644 --- a/src/hooks/useRefreshStation.ts +++ b/src/hooks/useRefreshStation.ts @@ -16,6 +16,7 @@ import { useApproachingStation } from './useApproachingStation'; import { useCanGoForward } from './useCanGoForward'; import { useNearestStation } from './useNearestStation'; import { useNextStation } from './useNextStation'; +import { usePassedStation } from './usePassedStation'; import { useStationNumberIndexFunc } from './useStationNumberIndexFunc'; import { useThreshold } from './useThreshold'; import { useWrongDirectionDetector } from './useWrongDirectionDetector'; @@ -53,6 +54,8 @@ export const useRefreshStation = (): void => { useAtomValue(notifyState); const nearestStation = useNearestStation(); + // 到着圏を取りこぼしたまま通り過ぎた通過駅。現在駅が後方に取り残されたときの自己修復に使う + const passedStation = usePassedStation(); const canGoForward = useCanGoForward(); const getStationNumberIndex = useStationNumberIndexFunc(); const { arrivedThreshold, approachingThreshold } = useThreshold(); @@ -239,10 +242,19 @@ export const useRefreshStation = (): void => { setStation((prev) => { const approaching = !isArrived && !getIsPass(nearestStation) && isApproaching; - const station = - isArrived && prev.station?.id !== nearestStation.id - ? nearestStation - : prev.station; + // 到着していない間に、到着圏を取りこぼした通過駅を通り過ぎていたら現在駅を進める。 + // 到着は扱わない(arrivedはfalseのまま)ので、到着通知・ETAの基準駅は動かない。 + const station = (() => { + if (isArrived) { + return prev.station?.id !== nearestStation.id + ? nearestStation + : prev.station; + } + if (passedStation && prev.station?.id !== passedStation.id) { + return passedStation; + } + return prev.station; + })(); if ( prev.approaching === approaching && prev.arrived === isArrived && @@ -260,5 +272,12 @@ export const useRefreshStation = (): void => { : prev ); } - }, [isApproaching, isArrived, nearestStation, setNavigation, setStation]); + }, [ + isApproaching, + isArrived, + nearestStation, + passedStation, + setNavigation, + setStation, + ]); }; diff --git a/src/screens/RideReview.tsx b/src/screens/RideReview.tsx index 3cd2350513..61cadd8460 100644 --- a/src/screens/RideReview.tsx +++ b/src/screens/RideReview.tsx @@ -496,7 +496,7 @@ const RouteMapCard = ({ const TopLines = ({ stats }: { stats: RideStats }) => { const colors = useAppColors(); const lines = stats.lines.slice(0, TOP_LINES_LIMIT); - const max = lines[0]?.distanceMeters ?? 0; + const max = lines[0]?.rideCount ?? 0; if (lines.length === 0) { return null; } @@ -530,7 +530,7 @@ const TopLines = ({ stats }: { stats: RideStats }) => { style={[ styles.lineFill, { - width: `${max > 0 ? (line.distanceMeters / max) * 100 : 0}%`, + width: `${max > 0 ? (line.rideCount / max) * 100 : 0}%`, backgroundColor: color, }, ]} diff --git a/src/utils/nativeTtsVoice.ts b/src/utils/nativeTtsVoice.ts index 517fe11e42..07d3347539 100644 --- a/src/utils/nativeTtsVoice.ts +++ b/src/utils/nativeTtsVoice.ts @@ -24,10 +24,10 @@ import { type Voice, VoiceQuality } from 'expo-speech'; // QUALITY_NORMAL へ並ぶため、expo-speech が返す 2 値の quality だけでは優劣を // 判定できず、識別子のアルファベット順という無根拠なタイブレークで音声が決まって // しまう('ja-jp-x-htm-local' が常に勝ち、ユーザーが端末設定で選んだ音声も無視 -// される)。そのため expo-speech の Android 実装へ patch を当てて生の -// Voice メタデータ(qualityScore / isDefault / networkRequired / notInstalled) -// を受け取り、それらを優先順の判断材料にする。iOS ではこれらのフィールドは -// 返らないため、従来どおり識別子と quality で判定する。 +// される)。生の Voice メタデータ(qualityScore / isDefault / networkRequired / +// notInstalled)が届けば優先順の判断材料にするが、現在の expo-speech はどの +// プラットフォームでも返さないため、識別子と quality で判定することになる +// (docs/spec/tts/remote-tts.md「端末内蔵 TTS の音声選択」)。 // // 注意: expo-speech の iOS 実装は AVSpeechSynthesisVoiceQuality.premium を // "Default" として報告する(native 側が enhanced 以外を一律 Default に落とす)ため、 @@ -35,8 +35,8 @@ import { type Voice, VoiceQuality } from 'expo-speech'; // `com.apple.voice.premium.ja-JP.Kyoko` / `com.apple.ttsbundle.Kyoko-premium` の // ように品質を含む命名になっているので、識別子でも判定して補完する。 -// expo-speech の Voice 型は Android patch で追加したフィールドを含まないため、 -// アプリ側で拡張して扱う。iOS ではいずれも返らないので optional にしている。 +// expo-speech の Voice 型はこれらのフィールドを含まないため、アプリ側で拡張して扱う。 +// 現在はどのプラットフォームでも返らないので optional にしている。 export type NativeVoice = Voice & { // android.speech.tts.Voice.getQuality() の生値 (VERY_LOW=100 〜 VERY_HIGH=500) qualityScore?: number; diff --git a/src/utils/passedStation.gpx.test.ts b/src/utils/passedStation.gpx.test.ts new file mode 100644 index 0000000000..f8e77e9747 --- /dev/null +++ b/src/utils/passedStation.gpx.test.ts @@ -0,0 +1,162 @@ +import { type Station, StopCondition } from '~/@types/graphql'; +import { ARRIVED_MAX_THRESHOLD } from '~/constants'; +import { getAccuracyBonus } from './accuracyBonus'; +import { findPassedStation } from './passedStation'; +import { createStation } from './test/factories'; +import { + METERS_PER_DEG_LAT, + makeNoise, + parseGpx, + type TrackPoint, + trackDistance, + truthAt, +} from './test/gpxTrack'; + +// 東北新幹線 盛岡→仙台(最高320km/h)。一ノ関だけに停車し、5駅を通過する。 +// 駅の座標はGPXの軌跡上で各駅に最も近い点を使う(GPXは駅の座標を通るよう生成されている)。 +const STATIONS: { name: string; index: number; pass: boolean }[] = [ + { name: '盛岡', index: 0, pass: false }, + { name: '新花巻', index: 416, pass: true }, + { name: '北上', index: 579, pass: true }, + { name: '水沢江刺', index: 777, pass: true }, + { name: '一ノ関', index: 1159, pass: false }, + { name: 'くりこま高原', index: 1428, pass: true }, + { name: '古川', index: 1673, pass: true }, + { name: '仙台', index: 2131, pass: false }, +]; + +// 背景ではiOSが約10秒ぶんの測位をまとめて届け、index.js はその最後の1点だけを使う +const BATCH_INTERVAL_MS = 10_000; +const ACCURACY = 10; +// 通過駅をこの距離だけ過ぎても現在駅が手前に残っていたら、取りこぼしとみなす +const MAX_LAG_METERS = 2_000; + +const track = parseGpx('assets/gpx/SampleTohokuShinkansen.gpx'); + +const stations: Station[] = STATIONS.map(({ index, pass }, i) => + createStation(i + 1, { + latitude: track[index].lat, + longitude: track[index].lon, + stopCondition: pass ? StopCondition.Not : StopCondition.All, + }) +); + +// 軌跡に沿った累積距離(m) +const cumulative = track.reduce((acc, p, i) => { + acc.push(i === 0 ? 0 : acc[i - 1] + trackDistance(track[i - 1], p)); + return acc; +}, []); + +const alongTrackAt = (t: number): number => { + const i = track.findIndex((p) => p.t > t); + if (i <= 0) { + return i === 0 ? 0 : cumulative[cumulative.length - 1]; + } + const a = track[i - 1]; + const b = track[i]; + return ( + cumulative[i - 1] + + (cumulative[i] - cumulative[i - 1]) * ((t - a.t) / (b.t - a.t)) + ); +}; + +const stationAlongTrack = STATIONS.map(({ index }) => cumulative[index]); +const arrivedRadius = ARRIVED_MAX_THRESHOLD + getAccuracyBonus(ACCURACY); + +/** + * useRefreshStation の現在駅の更新を、測位をまとめて受け取る間隔で再現する。 + * 最寄り駅の到着圏に入ればその駅へ、入らなければ(withPassedStationのとき)通り過ぎた + * 通過駅へ進める。 + */ +const ride = (phaseMs: number, withPassedStation: boolean) => { + const noise = makeNoise(phaseMs + 1); + let current: Station = stations[0]; + const samples: { along: number; currentIndex: number }[] = []; + // 通過駅の推定で現在駅を進めた瞬間の、進めた先の駅と実際の位置 + const advances: { stationIndex: number; along: number }[] = []; + + for ( + let t = track[0].t + phaseMs; + t <= track[track.length - 1].t; + t += BATCH_INTERVAL_MS + ) { + const truth: TrackPoint = truthAt(track, t); + const latitude = + truth.lat + ((noise() - 0.5) * 2 * ACCURACY) / METERS_PER_DEG_LAT; + const longitude = + truth.lon + + ((noise() - 0.5) * 2 * ACCURACY) / + (METERS_PER_DEG_LAT * Math.cos((truth.lat * Math.PI) / 180)); + const position = { lat: latitude, lon: longitude }; + const along = alongTrackAt(t); + + const arrived = stations.find( + (s) => + trackDistance(position, { + lat: s.latitude as number, + lon: s.longitude as number, + }) <= arrivedRadius + ); + if (arrived) { + current = arrived; + } else if (withPassedStation) { + const passed = findPassedStation( + stations, + current, + latitude, + longitude, + ACCURACY + ); + if (passed && passed !== current) { + current = passed; + advances.push({ stationIndex: stations.indexOf(passed), along }); + } + } + + samples.push({ along, currentIndex: stations.indexOf(current) }); + } + return { samples, advances }; +}; + +// 現在地より手前にある駅のうち、MAX_LAG_METERS 以上前に通り過ぎた最後の駅 +const lastClearlyPassedIndex = (along: number): number => + stationAlongTrack.reduce( + (last, d, i) => (d + MAX_LAG_METERS <= along ? i : last), + 0 + ); + +const PHASES = Array.from({ length: 10 }, (_, i) => i * 1_000); + +describe('findPassedStation (SampleTohokuShinkansen.gpx)', () => { + it('前提: 到着判定だけでは、背景の配信間隔で通過駅を取りこぼす', () => { + const lagged = PHASES.filter((phase) => + ride(phase, false).samples.some( + (s) => s.currentIndex < lastClearlyPassedIndex(s.along) + ) + ); + expect(lagged.length).toBeGreaterThan(0); + }); + + it.each(PHASES)( + '配信の位相 %ims: 通過駅を過ぎて2km以内に現在駅が追いつく', + (phase) => { + for (const s of ride(phase, true).samples) { + expect(s.currentIndex).toBeGreaterThanOrEqual( + lastClearlyPassedIndex(s.along) + ); + } + } + ); + + it.each(PHASES)( + '配信の位相 %ims: 通過駅は実際に通り過ぎてから進める', + (phase) => { + const { advances } = ride(phase, true); + expect(advances.length).toBeGreaterThan(0); + for (const a of advances) { + expect(STATIONS[a.stationIndex].pass).toBe(true); + expect(stationAlongTrack[a.stationIndex]).toBeLessThan(a.along); + } + } + ); +}); diff --git a/src/utils/passedStation.test.ts b/src/utils/passedStation.test.ts new file mode 100644 index 0000000000..39567cf466 --- /dev/null +++ b/src/utils/passedStation.test.ts @@ -0,0 +1,102 @@ +import { StopCondition } from '~/@types/graphql'; +import { createStation } from '~/utils/test/factories'; +import { findPassedStation } from './passedStation'; + +// 東海道新幹線 上り(三島→新横浜)。のぞみは熱海・小田原を通過する。 +const mishima = createStation(100206, { + name: '三島', + latitude: 35.1265, + longitude: 138.9108, +}); +const atami = createStation(100205, { + name: '熱海', + latitude: 35.1036, + longitude: 139.0779, + stopCondition: StopCondition.Not, +}); +const odawara = createStation(100204, { + name: '小田原', + latitude: 35.2564, + longitude: 139.1552, + stopCondition: StopCondition.Not, +}); +const shinYokohama = createStation(100203, { + name: '新横浜', + latitude: 35.5069, + longitude: 139.6175, +}); + +const nozomi = [mishima, atami, odawara, shinYokohama]; + +// 診断ダンプ(2026-10-04)の測位。小田原→新横浜の間で、現在駅は三島に残っていた +const BETWEEN_ODAWARA_AND_SHIN_YOKOHAMA = { + latitude: 35.40113, + longitude: 139.39607, +}; + +describe('findPassedStation', () => { + it('通過駅を2つ取りこぼしても、現在地の区間の始点まで進める', () => { + const { latitude, longitude } = BETWEEN_ODAWARA_AND_SHIN_YOKOHAMA; + expect(findPassedStation(nozomi, mishima, latitude, longitude, 7)?.id).toBe( + odawara.id + ); + }); + + it('熱海と小田原の間なら熱海まで進める', () => { + expect(findPassedStation(nozomi, mishima, 35.18, 139.12, 10)?.id).toBe( + atami.id + ); + }); + + it('現在駅と次駅の間にいる間は進めない', () => { + expect( + findPassedStation(nozomi, mishima, 35.115, 139.0, 10) + ).toBeUndefined(); + }); + + it('通過駅の到着圏が取りうる最大半径(350m)の内側では進めない', () => { + // 小田原から新横浜方向へ約300m + expect( + findPassedStation(nozomi, mishima, 35.2575, 139.1582, 5) + ).toBeUndefined(); + }); + + it('測位誤差を差し引くと到着圏に届きうる場合は進めない', () => { + // 小田原から約1.4kmだが、精度1500mでは手前にいる可能性を否定できない + expect( + findPassedStation(nozomi, mishima, 35.262, 139.17, 1500) + ).toBeUndefined(); + }); + + it('停車駅は飛び越えない', () => { + const kodama = [ + mishima, + atami, + { ...odawara, stopCondition: StopCondition.All }, + shinYokohama, + ]; + const { latitude, longitude } = BETWEEN_ODAWARA_AND_SHIN_YOKOHAMA; + expect( + findPassedStation(kodama, mishima, latitude, longitude, 7) + ).toBeUndefined(); + }); + + it('現在駅より後方にいるときは進めない', () => { + expect( + findPassedStation(nozomi, mishima, 35.14, 138.85, 10) + ).toBeUndefined(); + }); + + it('現在駅が並びに無ければ何もしない', () => { + const { latitude, longitude } = BETWEEN_ODAWARA_AND_SHIN_YOKOHAMA; + expect( + findPassedStation( + nozomi, + createStation(1, { latitude: 35, longitude: 139 }), + latitude, + longitude, + 7 + ) + ).toBeUndefined(); + }); +}); diff --git a/src/utils/passedStation.ts b/src/utils/passedStation.ts new file mode 100644 index 0000000000..3fd52dfda9 --- /dev/null +++ b/src/utils/passedStation.ts @@ -0,0 +1,113 @@ +import getDistance from 'geolib/es/getPreciseDistance'; +import type { Station } from '~/@types/graphql'; +import { ARRIVED_MAX_THRESHOLD } from '~/constants'; +import { getAccuracyBonus } from './accuracyBonus'; +import getIsPass from './isPass'; + +// 通過したとみなすために、通過駅から最低限離れていなければならない距離(m)。 +// 到着圏が取りうる最大の半径(ARRIVED_MAX_THRESHOLD + 精度補正の上限)より外に置く。 +// その内側では通常の到着判定がまだ通過駅を拾える。また駅の手前と奥の区別がつかない +// (カーブ上で次区間の方が近く見える)のも駅のごく近くに限られる。 +const PASSED_STATION_MIN_DISTANCE = + ARRIVED_MAX_THRESHOLD + getAccuracyBonus(Number.MAX_VALUE); + +type Coordinates = { latitude: number; longitude: number }; + +const toCoordinates = (s: Station | undefined): Coordinates | null => + s?.latitude != null && s.longitude != null + ? { latitude: s.latitude, longitude: s.longitude } + : null; + +/** + * 現在駅より先にある通過駅のうち、現在地がすでに通り過ぎたものを返す。 + * + * 現在駅(stationState.station)は、最寄り駅の到着圏(半径200m前後)に測位が入ったときにしか + * 進まない。高速で通過する駅では到着圏を横切る時間が数秒しかなく、背景で測位がまとめて + * 届く間隔(約10秒)より短いため、通過駅を取りこぼして現在駅が後方に取り残される。 + * 取り残されると次駅・駅間の閾値・路線図上の現在位置が揃ってずれたままになる。 + * + * 現在地が載っている区間を、進行方向に並んだ駅間のうち「両端までの距離の和と駅間距離の + * 差」が最小のものとして選ぶ。その区間の始点が現在駅より先なら、始点を通過したとみなす。 + * + * 現在駅の先に続く通過駅の区間だけを候補にし、停車駅は飛び越えない。停車駅は停車中に + * 測位が届き続けるので到着判定で拾えるはずで、拾えていないなら測位の側を疑う方が安全。 + * 停車駅へ進めると到着通知やETAの基準駅まで動いてしまう。 + * + * @param orderedStations 進行方向順に並んだ駅(ループ線は対象外) + */ +export const findPassedStation = ( + orderedStations: Station[], + currentStation: Station | null | undefined, + latitude: number | null | undefined, + longitude: number | null | undefined, + accuracy: number | null | undefined +): Station | undefined => { + if (!currentStation || latitude == null || longitude == null) { + return undefined; + } + + const idMatchIndex = orderedStations.findIndex( + (s) => s.id === currentStation.id + ); + const currentIndex = + idMatchIndex !== -1 + ? idMatchIndex + : orderedStations.findIndex((s) => s.groupId === currentStation.groupId); + if (currentIndex === -1) { + return undefined; + } + + const current: Coordinates = { latitude, longitude }; + let bestIndex = -1; + let bestExcess = Number.POSITIVE_INFINITY; + + for (let i = currentIndex; i < orderedStations.length - 1; i++) { + const start = orderedStations[i]; + // 現在駅より先は、通過駅が続く間だけを候補にする + if (i > currentIndex && !getIsPass(start)) { + break; + } + const startCoordinates = toCoordinates(start); + const endCoordinates = toCoordinates(orderedStations[i + 1]); + if (!startCoordinates || !endCoordinates) { + continue; + } + const excess = + getDistance(current, startCoordinates) + + getDistance(current, endCoordinates) - + getDistance(startCoordinates, endCoordinates); + if (excess < bestExcess) { + bestExcess = excess; + bestIndex = i; + } + } + + if (bestIndex <= currentIndex) { + return undefined; + } + + const passed = orderedStations[bestIndex]; + const passedCoordinates = toCoordinates(passed); + const nextCoordinates = toCoordinates(orderedStations[bestIndex + 1]); + if (!passed || !passedCoordinates || !nextCoordinates) { + return undefined; + } + + // 測位の誤差ぶんは手前にいる可能性があるとみなし、誤差を差し引いても + // 通過駅の到着圏の外で、かつ通過駅より次の駅に近い場合だけ通過を確定する + const uncertainty = + accuracy != null && Number.isFinite(accuracy) && accuracy > 0 + ? accuracy + : 0; + const distanceFromPassed = getDistance(current, passedCoordinates); + const distanceToNext = getDistance(current, nextCoordinates); + const sectionDistance = getDistance(passedCoordinates, nextCoordinates); + if (distanceFromPassed - uncertainty < PASSED_STATION_MIN_DISTANCE) { + return undefined; + } + if (distanceToNext + uncertainty >= sectionDistance) { + return undefined; + } + + return passed; +}; diff --git a/src/utils/rideStats.test.ts b/src/utils/rideStats.test.ts index c3dbb31ac9..8d6aff7a2e 100644 --- a/src/utils/rideStats.test.ts +++ b/src/utils/rideStats.test.ts @@ -200,7 +200,7 @@ describe('summarizeRides', () => { expect(year.buckets[1].distanceMeters).toBe(1000); }); - it('路線ごとの距離は到着した側の駅の路線に数え、長い順に並べる', () => { + it('路線ごとの距離は到着した側の駅の路線に数え、同じ回数なら長い順に並べる', () => { const start = at('2026-09-28T08:00:00Z'); // 中央線快速から総武線へ直通する乗車(中央線 1000m・総武線 2000m) const through = ride('through', start, [ @@ -217,20 +217,25 @@ describe('summarizeRides', () => { ]); }); - it('同じ距離の路線は乗った回数の多い順に並べる', () => { - const start = at('2026-09-28T08:00:00Z'); - const through = ride('through', start, [ - {}, - { lineId: 22, lineName: '総武線', lineColor: '#FFD400' }, - { lineId: 22, lineName: '総武線', lineColor: '#FFD400' }, - ]); - const chuo = ride('chuo', at('2026-09-29T08:00:00Z'), [{}]); - const stats = summarizeRides([through, chuo], 'week', week); + it('距離が短くても乗った回数の多い路線を先に並べる', () => { + // 総武線を1回で5000m、中央線快速を2回で計2000m + const sobu = ride( + 'sobu', + at('2026-09-28T08:00:00Z'), + Array.from({ length: 5 }, () => ({ + lineId: 22, + lineName: '総武線', + lineColor: '#FFD400', + })) + ); + const chuo1 = ride('chuo1', at('2026-09-29T08:00:00Z'), [{}]); + const chuo2 = ride('chuo2', at('2026-09-30T08:00:00Z'), [{}]); + const stats = summarizeRides([sobu, chuo1, chuo2], 'week', week); expect( stats.lines.map((l) => [l.lineId, l.distanceMeters, l.rideCount]) ).toEqual([ [11, 2000, 2], - [22, 2000, 1], + [22, 5000, 1], ]); }); }); diff --git a/src/utils/rideStats.ts b/src/utils/rideStats.ts index e0d521b875..6eafa62b84 100644 --- a/src/utils/rideStats.ts +++ b/src/utils/rideStats.ts @@ -128,7 +128,7 @@ export type RideStats = { rideCount: number; // グラフ用。週は曜日ごとの7つ、月は日ごとの日数ぶん、年は月ごとの12個 buckets: RideStatsBucket[]; - // 距離の長い順。同じ距離なら乗った回数の多い順 + // 乗った回数の多い順。同じ回数なら距離の長い順 lines: RideLineStats[]; }; @@ -220,7 +220,7 @@ export const summarizeRides = ( rideCount, buckets, lines: [...lines.values()].sort( - (a, b) => b.distanceMeters - a.distanceMeters || b.rideCount - a.rideCount + (a, b) => b.rideCount - a.rideCount || b.distanceMeters - a.distanceMeters ), }; };