Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44bc27a366 | ||
|
|
0d6c11e2bd | ||
|
|
92445e681f | ||
|
|
78cc909ec8 | ||
|
|
077e0e4266 | ||
|
|
0bf7033c6c | ||
|
|
0f98b6bcf7 | ||
|
|
8894a73999 | ||
|
|
cd4b58e16b | ||
|
|
39effddfc1 | ||
|
|
c4b06f3b3c | ||
|
|
eb33fa7feb | ||
|
|
65bf7b52c6 |
147
.gitignore
vendored
147
.gitignore
vendored
@@ -19,4 +19,149 @@ local.properties
|
|||||||
*.keystore
|
*.keystore
|
||||||
*.base64
|
*.base64
|
||||||
docs
|
docs
|
||||||
gradle.properties
|
linux
|
||||||
|
windows
|
||||||
|
flutter/ephemeral/
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
*.[Cc]ache
|
||||||
|
!*.[Cc]ache/
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
local.properties
|
||||||
|
*.log
|
||||||
|
captures/
|
||||||
|
.externalNativeBuild/
|
||||||
|
.cxx/
|
||||||
|
*.apk
|
||||||
|
output.json
|
||||||
|
*.iml
|
||||||
|
.idea/
|
||||||
|
misc.xml
|
||||||
|
deploymentTargetDropDown.xml
|
||||||
|
render.experimental.xml
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
google-services.json
|
||||||
|
*.hprof
|
||||||
|
gen-external-apklibs
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.fvm/flutter_sdk
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
coverage/
|
||||||
|
lib/generated_plugin_registrant.dart
|
||||||
|
**/android/**/gradle-wrapper.jar
|
||||||
|
**/android/.gradle
|
||||||
|
**/android/captures/
|
||||||
|
**/android/gradlew
|
||||||
|
**/android/gradlew.bat
|
||||||
|
**/android/key.properties
|
||||||
|
**/android/local.properties
|
||||||
|
**/android/**/GeneratedPluginRegistrant.java
|
||||||
|
**/ios/**/*.mode1v3
|
||||||
|
**/ios/**/*.mode2v3
|
||||||
|
**/ios/**/*.moved-aside
|
||||||
|
**/ios/**/*.pbxuser
|
||||||
|
**/ios/**/*.perspectivev3
|
||||||
|
**/ios/**/*sync/
|
||||||
|
**/ios/**/.sconsign.dblite
|
||||||
|
**/ios/**/.tags*
|
||||||
|
**/ios/**/.vagrant/
|
||||||
|
**/ios/**/DerivedData/
|
||||||
|
**/ios/**/Icon?
|
||||||
|
**/ios/**/Pods/
|
||||||
|
**/ios/**/.symlinks/
|
||||||
|
**/ios/**/profile
|
||||||
|
**/ios/**/xcuserdata
|
||||||
|
**/ios/.generated/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
**/ios/Flutter/App.framework
|
||||||
|
**/ios/Flutter/Flutter.framework
|
||||||
|
**/ios/Flutter/Flutter.podspec
|
||||||
|
**/ios/Flutter/Generated.xcconfig
|
||||||
|
**/ios/Flutter/app.flx
|
||||||
|
**/ios/Flutter/app.zip
|
||||||
|
**/ios/Flutter/flutter_assets/
|
||||||
|
**/ios/Flutter/flutter_export_environment.sh
|
||||||
|
**/ios/ServiceDefinitions.json
|
||||||
|
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||||
|
!**/ios/**/default.mode1v3
|
||||||
|
!**/ios/**/default.mode2v3
|
||||||
|
!**/ios/**/default.pbxuser
|
||||||
|
!**/ios/**/default.perspectivev3
|
||||||
|
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
*.dex
|
||||||
|
*.class
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
.gradle
|
||||||
|
.signing/
|
||||||
|
proguard/
|
||||||
|
/*/build/
|
||||||
|
/*/local.properties
|
||||||
|
/*/out
|
||||||
|
/*/*/build
|
||||||
|
/*/*/production
|
||||||
|
.navigation/
|
||||||
|
*.ipr
|
||||||
|
*~
|
||||||
|
*.swp
|
||||||
|
.externalNativeBuild
|
||||||
|
obj/
|
||||||
|
*.iws
|
||||||
|
/out/
|
||||||
|
.idea/caches/
|
||||||
|
.idea/libraries/
|
||||||
|
.idea/shelf/
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/tasks.xml
|
||||||
|
.idea/.name
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/copyright/profiles_settings.xml
|
||||||
|
.idea/encodings.xml
|
||||||
|
.idea/misc.xml
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/scopes/scope_settings.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
.idea/vcs.xml
|
||||||
|
.idea/jsLibraryMappings.xml
|
||||||
|
.idea/datasources.xml
|
||||||
|
.idea/dataSources.ids
|
||||||
|
.idea/sqlDataSources.xml
|
||||||
|
.idea/dynamic.xml
|
||||||
|
.idea/uiDesigner.xml
|
||||||
|
.idea/assetWizardSettings.xml
|
||||||
|
.idea/gradle.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/navEditor.xml
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.cproject
|
||||||
|
.settings/
|
||||||
|
.mtj.tmp/
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
hs_err_pid*
|
||||||
|
.idea_modules/
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
.idea/mongoSettings.xml
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
!/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
macos/Flutter/ephemeral/flutter_export_environment.sh
|
||||||
|
macos/Flutter/ephemeral/Flutter-Generated.xcconfig
|
||||||
|
*.py
|
||||||
2
.idea/.name
generated
2
.idea/.name
generated
@@ -1 +1 @@
|
|||||||
LBJ Receiver
|
LBJ_Console
|
||||||
6
.idea/deploymentTargetSelector.xml
generated
6
.idea/deploymentTargetSelector.xml
generated
@@ -2,6 +2,12 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="deploymentTargetSelector">
|
<component name="deploymentTargetSelector">
|
||||||
<selectionStates>
|
<selectionStates>
|
||||||
|
<SelectionState runConfigName="Unnamed">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
</SelectionState>
|
||||||
|
<SelectionState runConfigName="lbjconsole_android">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
</SelectionState>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
# LBJ Console
|
# LBJ Console
|
||||||
|
|
||||||
LBJ Console 是一款 Android 应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) device 设备接收并显示列车预警消息,功能包括:
|
LBJ Console 是一款应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) 设备接收并显示列车预警消息,功能包括:
|
||||||
|
|
||||||
- 接收列车预警消息,支持可选的手机推送通知。
|
- 接收列车预警消息,支持可选的手机推送通知。
|
||||||
- 显示预警消息的 GPS 信息于地图。
|
- 在地图上显示预警消息的 GPS 信息。
|
||||||
- 基于内置数据文件显示机车配属和车次类型。
|
- 基于内置数据文件显示机车配属,机车类型和车次类型。
|
||||||
|
|
||||||
|
|
||||||
|
主分支目前只适配了 Android 。如需在其它平台上面使用,请参考 [flutter](https://github.com/undef-i/LBJ_Console/tree/flutter) 分支自行编译。
|
||||||
## 数据文件
|
## 数据文件
|
||||||
|
|
||||||
LBJ Console 依赖以下数据文件,位于 `app/src/main/assets/` 目录,用于支持机车配属和车次信息的展示:
|
LBJ Console 依赖以下数据文件,位于 `app/src/main/assets/` 目录,用于支持机车配属和车次信息的展示:
|
||||||
- `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注`。
|
- `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注`。
|
||||||
|
- `loco_type_info.csv`:包含机车类型编码信息,格式为 `机车类型编码,机车类型`。
|
||||||
- `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型`。
|
- `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型`。
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId = "org.noxylva.lbjconsole"
|
applicationId = "org.noxylva.lbjconsole"
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 10
|
versionCode = 15
|
||||||
versionName = "0.1.1"
|
versionName = "0.1.5"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -92,4 +92,5 @@ dependencies {
|
|||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
implementation(libs.androidx.startup.runtime)
|
implementation(libs.androidx.startup.runtime)
|
||||||
|
implementation("com.google.code.gson:gson:2.10.1")
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
||||||
|
|
||||||
@@ -42,11 +45,10 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".FilePickerActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="Settings"
|
android:theme="@style/Theme.LBJConsole"
|
||||||
android:parentActivityName=".MainActivity"
|
android:label="数据管理" />
|
||||||
android:theme="@style/Theme.LBJConsole" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".BackgroundService"
|
android:name=".BackgroundService"
|
||||||
|
|||||||
143
app/src/main/assets/loco_type_info.csv
Normal file
143
app/src/main/assets/loco_type_info.csv
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
001,解放
|
||||||
|
003,前进
|
||||||
|
005,建设
|
||||||
|
006,KD7
|
||||||
|
055,蓝箭控车
|
||||||
|
081,东风21
|
||||||
|
101,东风
|
||||||
|
102,东风2
|
||||||
|
103,东风3
|
||||||
|
104,东风4
|
||||||
|
105,东风4客
|
||||||
|
106,东风4C
|
||||||
|
107,东风5
|
||||||
|
108,东风5宽
|
||||||
|
109,东风6
|
||||||
|
110,东风7
|
||||||
|
111,东风8
|
||||||
|
112,东风9
|
||||||
|
113,东风10
|
||||||
|
114,东方红1
|
||||||
|
115,东方红2
|
||||||
|
116,东方红3
|
||||||
|
117,东方红5
|
||||||
|
118,北京
|
||||||
|
119,北京宽
|
||||||
|
120,ND2
|
||||||
|
121,ND3
|
||||||
|
122,ND4
|
||||||
|
123,ND5
|
||||||
|
124,NY5
|
||||||
|
125,NY6
|
||||||
|
126,NY7
|
||||||
|
127,轻油
|
||||||
|
128,东方红21
|
||||||
|
129,东风7B
|
||||||
|
130,东风5S
|
||||||
|
131,东风7C
|
||||||
|
132,东风7S
|
||||||
|
133,工矿1
|
||||||
|
134,工矿1F
|
||||||
|
135,东风4E
|
||||||
|
136,东风7D
|
||||||
|
137,工矿1A
|
||||||
|
138,东风11
|
||||||
|
139,天安
|
||||||
|
140,东风10F
|
||||||
|
141,东风4D
|
||||||
|
142,东风8B
|
||||||
|
143,东风12
|
||||||
|
144,东风7E
|
||||||
|
145,NYJ1
|
||||||
|
146,NZJ1
|
||||||
|
147,NZJ2
|
||||||
|
148,东风4DJ
|
||||||
|
149,新曙光
|
||||||
|
150,神州
|
||||||
|
151,NJ2
|
||||||
|
152,东风7G
|
||||||
|
153,NDJ3
|
||||||
|
157,FXN3D
|
||||||
|
158,东风11G
|
||||||
|
160,HXN3
|
||||||
|
161,HXN5
|
||||||
|
162,HXN3B
|
||||||
|
163,HXN5B
|
||||||
|
167,FXN3B
|
||||||
|
169,FXN3C
|
||||||
|
170,FXN5C
|
||||||
|
171,FXN3-J
|
||||||
|
201,8G
|
||||||
|
202,8K
|
||||||
|
203,6G
|
||||||
|
204,6K
|
||||||
|
205,韶山1
|
||||||
|
206,韶山3
|
||||||
|
207,韶山4
|
||||||
|
208,韶山5
|
||||||
|
209,韶山6
|
||||||
|
210,韶山3B
|
||||||
|
211,韶山7
|
||||||
|
212,韶山8
|
||||||
|
213,韶山7B
|
||||||
|
214,韶山7C
|
||||||
|
215,韶山6B
|
||||||
|
216,韶山9
|
||||||
|
217,韶山7D
|
||||||
|
218,DJ熊猫
|
||||||
|
219,DJ1
|
||||||
|
220,DJ2
|
||||||
|
221,DJF
|
||||||
|
222,蓝箭动车
|
||||||
|
223,先锋号
|
||||||
|
224,韶山7E
|
||||||
|
225,韶山4G
|
||||||
|
226,韶山3C
|
||||||
|
228,天梭
|
||||||
|
229,DJ4和谐
|
||||||
|
230,KTT
|
||||||
|
231,HXD1
|
||||||
|
232,HXD2
|
||||||
|
233,HXD3
|
||||||
|
234,HXD1B
|
||||||
|
235,HXD2B
|
||||||
|
236,HXD3B
|
||||||
|
237,HXD1C
|
||||||
|
238,HXD2C
|
||||||
|
239,HXD3C
|
||||||
|
240,HXD1D
|
||||||
|
241,HXD2D
|
||||||
|
242,HXD3D
|
||||||
|
243,FXD1B
|
||||||
|
244,FXD2B
|
||||||
|
245,FXD1
|
||||||
|
246,FXD3
|
||||||
|
247,FXD1-J
|
||||||
|
248,FXD3-J
|
||||||
|
249,KZ25TA
|
||||||
|
251,KZ25TB
|
||||||
|
252,HXD1D-J
|
||||||
|
254,FXD1H
|
||||||
|
300,雪域神州
|
||||||
|
301,CRH1
|
||||||
|
302,CRH2
|
||||||
|
303,CRH3
|
||||||
|
305,CRH5
|
||||||
|
306,CRH380A
|
||||||
|
307,CRH380B
|
||||||
|
308,CRH380C
|
||||||
|
309,CRH380D
|
||||||
|
310,CRH6A
|
||||||
|
311,CR400AF
|
||||||
|
312,CR400BF
|
||||||
|
313,CR300AF
|
||||||
|
314,CR300BF
|
||||||
|
315,CRH2E
|
||||||
|
316,CRH6F
|
||||||
|
330,CJ1
|
||||||
|
331,CJ2
|
||||||
|
332,CJ3
|
||||||
|
333,CJ4
|
||||||
|
334,CJ5
|
||||||
|
335,CJ6
|
||||||
|
400,GCD-1000J
|
||||||
|
@@ -26,8 +26,8 @@
|
|||||||
"^[Vv1](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨三局及以上图定普通旅客快车"
|
"^[Vv1](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨三局及以上图定普通旅客快车"
|
||||||
"^[Bb2](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨两局图定普通旅客快车"
|
"^[Bb2](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨两局图定普通旅客快车"
|
||||||
"^3(00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨局临时普通旅客快车"
|
"^3(00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨局临时普通旅客快车"
|
||||||
"^[Uu4](00[1-9]|0[1-9]\d|[1-9]\d{2})$","管内图定普通旅客快车四字头"
|
"^[Uu4](00[1-9]|0[1-9]\d|[1-9]\d{2})$","管内图定普通旅客快车"
|
||||||
"^[Xx5]([0-8]\d{2}|9[0-8]\d|99[0-8])$","管内图定普通旅客快车五字头"
|
"^[Xx5](000|1[9][9]|200|3[9][9]|400)$","管内图定普通旅客快车"
|
||||||
"^6(19[0-8]|1[0-8]\d|0[1-9]\d|00[1-9])$","直通普通旅客慢车"
|
"^6(19[0-8]|1[0-8]\d|0[1-9]\d|00[1-9])$","直通普通旅客慢车"
|
||||||
"^(6(20[1-9]|2[1-9]\d|[3-9]\d{2})|7([0-4]\d{2}|5([0-8]\d|9[0-8])))$","管内普通旅客慢车"
|
"^(6(20[1-9]|2[1-9]\d|[3-9]\d{2})|7([0-4]\d{2}|5([0-8]\d|9[0-8])))$","管内普通旅客慢车"
|
||||||
"^(8([0-8]\d{2}|9[0-8]\d|99[0-8])|7(60[1-9]|6[1-9]\d|[7-9]\d{2}))$","通勤列车"
|
"^(8([0-8]\d{2}|9[0-8]\d|99[0-8])|7(60[1-9]|6[1-9]\d|[7-9]\d{2}))$","通勤列车"
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
"^DJ([4-9]\d{2}|40[1-9]|4[1-9]\d)$","动车组检测列车300管内"
|
"^DJ([4-9]\d{2}|40[1-9]|4[1-9]\d)$","动车组检测列车300管内"
|
||||||
"^DJ1(400|[0-3]\d{2})$","动车组检测列车250直通"
|
"^DJ1(400|[0-3]\d{2})$","动车组检测列车250直通"
|
||||||
"^DJ1(40[1-9]|4[1-9]\d|[5-9]\d{2})$","动车组检测列车250管内"
|
"^DJ1(40[1-9]|4[1-9]\d|[5-9]\d{2})$","动车组检测列车250管内"
|
||||||
"^DJ[56]\d{3}$","动车组确认列车直通"
|
"^DJ[56]\d{3}$","直通动车组确认列车"
|
||||||
"^DJ[78]\d{3}$","动车组确认列车管内"
|
"^DJ[78]\d{3}$","管内动车组确认列车"
|
||||||
"^[Ff][GDCZTKgdcztk]?\d{1,4}$","因故折返旅客列车"
|
"^[Ff][GDCZTKgdcztk]?\d{1,4}$","因故折返旅客列车"
|
||||||
"^0[GDCZTKgdcztk]\d{1,4}$","回送图定客车底"
|
"^0[GDCZTKgdcztk]\d{1,4}$","回送图定客车底"
|
||||||
"^00(100|[1-9]\d?)$","有火回送动车组车底"
|
"^00(100|[1-9]\d?)$","有火回送动车组车底"
|
||||||
|
|||||||
|
123
app/src/main/java/org/noxylva/lbjconsole/FilePickerActivity.kt
Normal file
123
app/src/main/java/org/noxylva/lbjconsole/FilePickerActivity.kt
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package org.noxylva.lbjconsole
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.noxylva.lbjconsole.util.DatabaseExportImportUtil
|
||||||
|
|
||||||
|
class FilePickerActivity : ComponentActivity() {
|
||||||
|
private val exportFilePicker = registerForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument("application/json")
|
||||||
|
) { uri ->
|
||||||
|
uri?.let { exportDatabase(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val importFilePicker = registerForActivityResult(
|
||||||
|
ActivityResultContracts.OpenDocument()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let { importDatabase(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val action = intent.getStringExtra("action") ?: "export"
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
when (action) {
|
||||||
|
"export" -> {
|
||||||
|
val fileName = "lbj_console_backup_${System.currentTimeMillis()}.json"
|
||||||
|
exportFilePicker.launch(fileName)
|
||||||
|
}
|
||||||
|
"import" -> {
|
||||||
|
importFilePicker.launch(arrayOf("application/json", "text/plain"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exportDatabase(uri: Uri) {
|
||||||
|
val coroutineScope = kotlinx.coroutines.MainScope()
|
||||||
|
coroutineScope.launch {
|
||||||
|
try {
|
||||||
|
val databaseUtil = DatabaseExportImportUtil(this@FilePickerActivity)
|
||||||
|
val json = databaseUtil.exportDatabase()
|
||||||
|
|
||||||
|
contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
outputStream.write(json.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(
|
||||||
|
this@FilePickerActivity,
|
||||||
|
"数据导出成功",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@FilePickerActivity,
|
||||||
|
"导出失败: ${e.message}",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
} finally {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importDatabase(uri: Uri) {
|
||||||
|
val coroutineScope = kotlinx.coroutines.MainScope()
|
||||||
|
coroutineScope.launch {
|
||||||
|
try {
|
||||||
|
val databaseUtil = DatabaseExportImportUtil(this@FilePickerActivity)
|
||||||
|
val success = databaseUtil.importDatabase(uri)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@FilePickerActivity,
|
||||||
|
"数据导入成功",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
this@FilePickerActivity,
|
||||||
|
"数据导入失败",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@FilePickerActivity,
|
||||||
|
"导入失败: ${e.message}",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
} finally {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun createExportIntent(context: android.content.Context): Intent {
|
||||||
|
return Intent(context, FilePickerActivity::class.java).apply {
|
||||||
|
putExtra("action", "export")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createImportIntent(context: android.content.Context): Intent {
|
||||||
|
return Intent(context, FilePickerActivity::class.java).apply {
|
||||||
|
putExtra("action", "import")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,16 +110,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
private var historyScrollPosition by mutableStateOf(0)
|
private var historyScrollPosition by mutableStateOf(0)
|
||||||
private var historyScrollOffset by mutableStateOf(0)
|
private var historyScrollOffset by mutableStateOf(0)
|
||||||
private var historyCardMapStates by mutableStateOf<Map<String, CardMapView>>(emptyMap())
|
private var historyCardMapStates by mutableStateOf<Map<String, CardMapView>>(emptyMap())
|
||||||
|
private var settingsScrollPosition by mutableStateOf(0)
|
||||||
private var mapCenterPosition by mutableStateOf<Pair<Double, Double>?>(null)
|
private var mapCenterPosition by mutableStateOf<Pair<Double, Double>?>(null)
|
||||||
private var mapZoomLevel by mutableStateOf(10.0)
|
private var mapZoomLevel by mutableStateOf(10.0)
|
||||||
private var mapRailwayLayerVisible by mutableStateOf(true)
|
private var mapRailwayLayerVisible by mutableStateOf(true)
|
||||||
|
|
||||||
private var settingsScrollPosition by mutableStateOf(0)
|
|
||||||
|
|
||||||
private var mergeSettings by mutableStateOf(MergeSettings())
|
private var mergeSettings by mutableStateOf(MergeSettings())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private var targetDeviceName = "LBJReceiver"
|
private var targetDeviceName = "LBJReceiver"
|
||||||
private var specifiedDeviceAddress by mutableStateOf<String?>(null)
|
private var specifiedDeviceAddress by mutableStateOf<String?>(null)
|
||||||
private var searchOrderList by mutableStateOf(listOf<String>())
|
private var searchOrderList by mutableStateOf(listOf<String>())
|
||||||
@@ -185,10 +183,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
TrainRecord.initializeLocoTypeUtil(this)
|
||||||
|
|
||||||
loadSettings()
|
loadSettings()
|
||||||
|
|
||||||
|
|
||||||
val permissions = mutableListOf<String>()
|
val permissions = mutableListOf<String>()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
@@ -198,100 +196,24 @@ class MainActivity : ComponentActivity() {
|
|||||||
Manifest.permission.BLUETOOTH_ADVERTISE
|
Manifest.permission.BLUETOOTH_ADVERTISE
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
permissions.addAll(arrayOf(
|
|
||||||
Manifest.permission.BLUETOOTH,
|
|
||||||
Manifest.permission.BLUETOOTH_ADMIN
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
permissions.addAll(arrayOf(
|
permissions.addAll(arrayOf(
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
))
|
))
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (permissions.isNotEmpty()) {
|
||||||
requestPermissions.launch(permissions.toTypedArray())
|
requestPermissions.launch(permissions.toTypedArray())
|
||||||
|
} else {
|
||||||
|
startAutoScanAndConnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
Configuration.getInstance().userAgentValue = packageName
|
||||||
|
|
||||||
bleClient.setTrainInfoCallback { jsonData ->
|
bleClient.setTrainInfoCallback { jsonData ->
|
||||||
handleTrainInfo(jsonData)
|
handleTrainInfo(jsonData)
|
||||||
}
|
}
|
||||||
|
|
||||||
bleClient.setHighFrequencyReconnect(true)
|
|
||||||
bleClient.setConnectionLostCallback {
|
|
||||||
runOnUiThread {
|
|
||||||
deviceStatus = "连接丢失,正在重连..."
|
|
||||||
showDisconnectButton = false
|
|
||||||
if (showConnectionDialog) {
|
|
||||||
foundDevices = emptyList()
|
|
||||||
startScan()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bleClient.setConnectionSuccessCallback { address ->
|
|
||||||
runOnUiThread {
|
|
||||||
deviceAddress = address
|
|
||||||
deviceStatus = "已连接"
|
|
||||||
showDisconnectButton = true
|
|
||||||
Log.d(TAG, "Connection success callback: address=$address")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
locoInfoUtil.loadLocoData()
|
|
||||||
Log.d(TAG, "Loaded locomotive data")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Load locomotive data failed", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
val osmCacheDir = File(cacheDir, "osm").apply { mkdirs() }
|
|
||||||
val tileCache = File(osmCacheDir, "tiles").apply { mkdirs() }
|
|
||||||
|
|
||||||
|
|
||||||
Configuration.getInstance().apply {
|
|
||||||
userAgentValue = packageName
|
|
||||||
load(this@MainActivity, getSharedPreferences("osmdroid", Context.MODE_PRIVATE))
|
|
||||||
osmdroidBasePath = osmCacheDir
|
|
||||||
osmdroidTileCache = tileCache
|
|
||||||
expirationOverrideDuration = 86400000L * 7
|
|
||||||
tileDownloadThreads = 4
|
|
||||||
tileFileSystemThreads = 4
|
|
||||||
|
|
||||||
setUserAgentValue("LBJConsole/1.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "OSM cache configured")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "OSM cache config failed", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSettings()
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (SettingsActivity.isBackgroundServiceEnabled(this@MainActivity)) {
|
|
||||||
BackgroundService.startService(this@MainActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enableEdgeToEdge()
|
|
||||||
|
|
||||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
|
||||||
isAppearanceLightStatusBars = false
|
|
||||||
}
|
|
||||||
setContent {
|
setContent {
|
||||||
LBJConsoleTheme {
|
LBJConsoleTheme {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -334,7 +256,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
Log.d(TAG, "Auto connect enabled: $enabled")
|
Log.d(TAG, "Auto connect enabled: $enabled")
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
latestRecord = latestRecord,
|
latestRecord = latestRecord,
|
||||||
recentRecords = recentRecords,
|
recentRecords = recentRecords,
|
||||||
lastUpdateTime = lastUpdateTime,
|
lastUpdateTime = lastUpdateTime,
|
||||||
@@ -344,10 +265,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
},
|
},
|
||||||
onClearMonitorLog = {
|
onClearMonitorLog = {
|
||||||
recentRecords.clear()
|
recentRecords.clear()
|
||||||
|
latestRecord = null
|
||||||
|
lastUpdateTime = null
|
||||||
temporaryStatusMessage = null
|
temporaryStatusMessage = null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
allRecords = trainRecordManager.getMixedRecords(),
|
allRecords = trainRecordManager.getMixedRecords(),
|
||||||
mergedRecords = trainRecordManager.getMergedRecords(),
|
mergedRecords = trainRecordManager.getMergedRecords(),
|
||||||
recordCount = trainRecordManager.getRecordCount(),
|
recordCount = trainRecordManager.getRecordCount(),
|
||||||
@@ -442,11 +364,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
},
|
},
|
||||||
appVersion = getAppVersion(),
|
appVersion = getAppVersion(),
|
||||||
locoInfoUtil = locoInfoUtil,
|
locoInfoUtil = locoInfoUtil,
|
||||||
trainTypeUtil = trainTypeUtil,
|
trainTypeUtil = trainTypeUtil
|
||||||
onOpenSettings = {
|
|
||||||
val intent = Intent(this@MainActivity, SettingsActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (showConnectionDialog) {
|
if (showConnectionDialog) {
|
||||||
@@ -499,7 +417,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -582,7 +499,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
|
|
||||||
private fun handleTrainInfo(jsonData: JSONObject) {
|
private fun handleTrainInfo(jsonData: JSONObject) {
|
||||||
Log.d(TAG, "Received train data=${jsonData.toString().take(50)}...")
|
Log.d(TAG, "Received train data=${jsonData.toString()}...")
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
try {
|
try {
|
||||||
@@ -762,7 +679,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val settings = appSettingsRepository.getSettings()
|
val settings = appSettingsRepository.getSettings()
|
||||||
|
|
||||||
settingsDeviceName = settings.deviceName
|
settingsDeviceName = settings.deviceName
|
||||||
targetDeviceName = settingsDeviceName
|
targetDeviceName = settings.deviceName
|
||||||
currentTab = settings.currentTab
|
currentTab = settings.currentTab
|
||||||
historyEditMode = settings.historyEditMode
|
historyEditMode = settings.historyEditMode
|
||||||
|
|
||||||
@@ -845,6 +762,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
Log.d(TAG, "onNewIntent called")
|
||||||
|
currentTab = 0
|
||||||
|
forceUiRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
Log.d(TAG, "App resumed")
|
Log.d(TAG, "App resumed")
|
||||||
@@ -949,9 +873,7 @@ fun MainContent(
|
|||||||
mapCenterPosition: Pair<Double, Double>?,
|
mapCenterPosition: Pair<Double, Double>?,
|
||||||
mapZoomLevel: Double,
|
mapZoomLevel: Double,
|
||||||
mapRailwayLayerVisible: Boolean,
|
mapRailwayLayerVisible: Boolean,
|
||||||
onMapStateChange: (Pair<Double, Double>?, Double, Boolean) -> Unit,
|
onMapStateChange: (Pair<Double, Double>?, Double, Boolean) -> Unit
|
||||||
|
|
||||||
onOpenSettings: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
val statusColor = if (isConnected) Color(0xFF4CAF50) else Color(0xFFFF5722)
|
val statusColor = if (isConnected) Color(0xFF4CAF50) else Color(0xFFFF5722)
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class NotificationService(private val context: Context) {
|
|||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
intent,
|
intent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
val remoteViews = RemoteViews(context.packageName, R.layout.notification_train_record)
|
val remoteViews = RemoteViews(context.packageName, R.layout.notification_train_record)
|
||||||
@@ -145,7 +145,7 @@ class NotificationService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isValidValue(trainRecord.position)) {
|
if (isValidValue(trainRecord.position)) {
|
||||||
remoteViews.setTextViewText(R.id.notification_position, "${trainRecord.position.trim()}K")
|
remoteViews.setTextViewText(R.id.notification_position, "${trainRecord.position.trim().removeSuffix(".")}K")
|
||||||
remoteViews.setViewVisibility(R.id.notification_position, View.VISIBLE)
|
remoteViews.setViewVisibility(R.id.notification_position, View.VISIBLE)
|
||||||
} else {
|
} else {
|
||||||
remoteViews.setViewVisibility(R.id.notification_position, View.GONE)
|
remoteViews.setViewVisibility(R.id.notification_position, View.GONE)
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
package org.noxylva.lbjconsole
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Switch
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.noxylva.lbjconsole.database.AppSettingsRepository
|
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
suspend fun isBackgroundServiceEnabled(context: Context): Boolean {
|
|
||||||
val repository = AppSettingsRepository(context)
|
|
||||||
return repository.getSettings().backgroundServiceEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setBackgroundServiceEnabled(context: Context, enabled: Boolean) {
|
|
||||||
val repository = AppSettingsRepository(context)
|
|
||||||
repository.updateBackgroundServiceEnabled(enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var backgroundServiceSwitch: Switch
|
|
||||||
private lateinit var appSettingsRepository: AppSettingsRepository
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_settings)
|
|
||||||
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
supportActionBar?.title = "Settings"
|
|
||||||
|
|
||||||
appSettingsRepository = AppSettingsRepository(this)
|
|
||||||
|
|
||||||
initViews()
|
|
||||||
setupListeners()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initViews() {
|
|
||||||
backgroundServiceSwitch = findViewById(R.id.switch_background_service)
|
|
||||||
lifecycleScope.launch {
|
|
||||||
backgroundServiceSwitch.isChecked = isBackgroundServiceEnabled(this@SettingsActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupListeners() {
|
|
||||||
backgroundServiceSwitch.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
lifecycleScope.launch {
|
|
||||||
setBackgroundServiceEnabled(this@SettingsActivity, isChecked)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isChecked) {
|
|
||||||
BackgroundService.startService(this)
|
|
||||||
} else {
|
|
||||||
BackgroundService.stopService(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
|
||||||
onBackPressed()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [TrainRecordEntity::class, AppSettingsEntity::class],
|
entities = [TrainRecordEntity::class, AppSettingsEntity::class],
|
||||||
version = 3,
|
version = 4,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
abstract class TrainDatabase : RoomDatabase() {
|
abstract class TrainDatabase : RoomDatabase() {
|
||||||
@@ -54,13 +54,89 @@ abstract class TrainDatabase : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Since we can't determine the exact schema change, we'll use fallback migration
|
||||||
|
// This will preserve data where possible while updating the schema
|
||||||
|
|
||||||
|
// Create new table with correct schema
|
||||||
|
database.execSQL("""
|
||||||
|
CREATE TABLE IF NOT EXISTS `app_settings_new` (
|
||||||
|
`id` INTEGER NOT NULL,
|
||||||
|
`deviceName` TEXT NOT NULL DEFAULT 'LBJReceiver',
|
||||||
|
`currentTab` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`historyEditMode` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`historySelectedRecords` TEXT NOT NULL DEFAULT '',
|
||||||
|
`historyExpandedStates` TEXT NOT NULL DEFAULT '',
|
||||||
|
`historyScrollPosition` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`historyScrollOffset` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`settingsScrollPosition` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`mapCenterLat` REAL,
|
||||||
|
`mapCenterLon` REAL,
|
||||||
|
`mapZoomLevel` REAL NOT NULL DEFAULT 10.0,
|
||||||
|
`mapRailwayLayerVisible` INTEGER NOT NULL DEFAULT 1,
|
||||||
|
`specifiedDeviceAddress` TEXT,
|
||||||
|
`searchOrderList` TEXT NOT NULL DEFAULT '',
|
||||||
|
`autoConnectEnabled` INTEGER NOT NULL DEFAULT 1,
|
||||||
|
`backgroundServiceEnabled` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`notificationEnabled` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
// Copy data from old table to new table, handling missing columns
|
||||||
|
try {
|
||||||
|
database.execSQL("""
|
||||||
|
INSERT INTO `app_settings_new` (
|
||||||
|
id, deviceName, currentTab, historyEditMode, historySelectedRecords,
|
||||||
|
historyExpandedStates, historyScrollPosition, historyScrollOffset,
|
||||||
|
settingsScrollPosition, mapCenterLat, mapCenterLon, mapZoomLevel,
|
||||||
|
mapRailwayLayerVisible
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COALESCE(id, 1),
|
||||||
|
COALESCE(deviceName, 'LBJReceiver'),
|
||||||
|
COALESCE(currentTab, 0),
|
||||||
|
COALESCE(historyEditMode, 0),
|
||||||
|
COALESCE(historySelectedRecords, ''),
|
||||||
|
COALESCE(historyExpandedStates, ''),
|
||||||
|
COALESCE(historyScrollPosition, 0),
|
||||||
|
COALESCE(historyScrollOffset, 0),
|
||||||
|
COALESCE(settingsScrollPosition, 0),
|
||||||
|
mapCenterLat,
|
||||||
|
mapCenterLon,
|
||||||
|
COALESCE(mapZoomLevel, 10.0),
|
||||||
|
COALESCE(mapRailwayLayerVisible, 1)
|
||||||
|
FROM `app_settings`
|
||||||
|
""")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If the old table doesn't exist or has different structure, insert default
|
||||||
|
database.execSQL("""
|
||||||
|
INSERT INTO `app_settings_new` (
|
||||||
|
id, deviceName, currentTab, historyEditMode, historySelectedRecords,
|
||||||
|
historyExpandedStates, historyScrollPosition, historyScrollOffset,
|
||||||
|
settingsScrollPosition, mapZoomLevel, mapRailwayLayerVisible,
|
||||||
|
searchOrderList, autoConnectEnabled, backgroundServiceEnabled,
|
||||||
|
notificationEnabled
|
||||||
|
) VALUES (
|
||||||
|
1, 'LBJReceiver', 0, 0, '', '', 0, 0, 0, 10.0, 1, '', 1, 0, 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop old table and rename new table
|
||||||
|
database.execSQL("DROP TABLE IF EXISTS `app_settings`")
|
||||||
|
database.execSQL("ALTER TABLE `app_settings_new` RENAME TO `app_settings`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getDatabase(context: Context): TrainDatabase {
|
fun getDatabase(context: Context): TrainDatabase {
|
||||||
return INSTANCE ?: synchronized(this) {
|
return INSTANCE ?: synchronized(this) {
|
||||||
val instance = Room.databaseBuilder(
|
val instance = Room.databaseBuilder(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
TrainDatabase::class.java,
|
TrainDatabase::class.java,
|
||||||
"train_database"
|
"train_database"
|
||||||
).addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
|
).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4).build()
|
||||||
INSTANCE = instance
|
INSTANCE = instance
|
||||||
instance
|
instance
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
package org.noxylva.lbjconsole.model
|
package org.noxylva.lbjconsole.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import org.osmdroid.util.GeoPoint
|
import org.osmdroid.util.GeoPoint
|
||||||
import org.noxylva.lbjconsole.util.LocationUtils
|
import org.noxylva.lbjconsole.util.LocationUtil
|
||||||
|
import org.noxylva.lbjconsole.util.LocoTypeUtil
|
||||||
|
|
||||||
class TrainRecord(jsonData: JSONObject? = null) {
|
class TrainRecord(jsonData: JSONObject? = null) {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "TrainRecord"
|
const val TAG = "TrainRecord"
|
||||||
private var nextId = 0L
|
private var nextId = 0L
|
||||||
|
private var LocoTypeUtil: LocoTypeUtil? = null
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun generateUniqueId(): String {
|
private fun generateUniqueId(): String {
|
||||||
return "${System.currentTimeMillis()}_${++nextId}"
|
return "${System.currentTimeMillis()}_${++nextId}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun initializeLocoTypeUtil(context: Context) {
|
||||||
|
if (LocoTypeUtil == null) {
|
||||||
|
LocoTypeUtil = LocoTypeUtil(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val uniqueId: String
|
val uniqueId: String
|
||||||
@@ -75,20 +84,25 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
|||||||
position = jsonData.optString("pos", "")
|
position = jsonData.optString("pos", "")
|
||||||
time = jsonData.optString("time", "")
|
time = jsonData.optString("time", "")
|
||||||
loco = jsonData.optString("loco", "")
|
loco = jsonData.optString("loco", "")
|
||||||
locoType = jsonData.optString("loco_type", "")
|
|
||||||
|
locoType = if (loco.isNotEmpty()) {
|
||||||
|
val prefix = if (loco.length >= 3) loco.take(3) else loco
|
||||||
|
LocoTypeUtil?.getLocoTypeByCode(prefix) ?: ""
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
lbjClass = jsonData.optString("lbj_class", "")
|
lbjClass = jsonData.optString("lbj_class", "")
|
||||||
route = jsonData.optString("route", "")
|
route = jsonData.optString("route", "")
|
||||||
positionInfo = jsonData.optString("position_info", "")
|
positionInfo = jsonData.optString("position_info", "")
|
||||||
rssi = jsonData.optDouble("rssi", 0.0)
|
rssi = jsonData.optDouble("rssi", 0.0)
|
||||||
|
|
||||||
|
|
||||||
_coordinates = null
|
_coordinates = null
|
||||||
|
|
||||||
Log.d(TAG, "Successfully parsed: train=$train, dir=$direction, speed=$speed, lbjClass='$lbjClass'")
|
Log.d(TAG, "Successfully parsed: train=$train, dir=$direction, speed=$speed, lbjClass='$lbjClass', locoType='$locoType'")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "JSON parse error: ${e.message}", e)
|
Log.e(TAG, "JSON parse error: ${e.message}", e)
|
||||||
|
|
||||||
|
|
||||||
try { train = jsonData.optString("train", "") } catch (e: Exception) { }
|
try { train = jsonData.optString("train", "") } catch (e: Exception) { }
|
||||||
try { direction = jsonData.optInt("dir", 0) } catch (e: Exception) { }
|
try { direction = jsonData.optInt("dir", 0) } catch (e: Exception) { }
|
||||||
try { speed = jsonData.optString("speed", "") } catch (e: Exception) { }
|
try { speed = jsonData.optString("speed", "") } catch (e: Exception) { }
|
||||||
@@ -107,7 +121,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_coordinates = LocationUtils.parsePositionInfo(positionInfo)
|
_coordinates = LocationUtil.parsePositionInfo(positionInfo)
|
||||||
return _coordinates
|
return _coordinates
|
||||||
}
|
}
|
||||||
private fun isValidValue(value: String): Boolean {
|
private fun isValidValue(value: String): Boolean {
|
||||||
@@ -134,7 +148,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
|||||||
lbjClass.trim()
|
lbjClass.trim()
|
||||||
} else if (isValidValue(train)) {
|
} else if (isValidValue(train)) {
|
||||||
train.trim()
|
train.trim()
|
||||||
} else ""
|
} else null
|
||||||
|
|
||||||
val map = mutableMapOf<String, String>()
|
val map = mutableMapOf<String, String>()
|
||||||
|
|
||||||
@@ -143,14 +157,17 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
|||||||
map["receivedTimestamp"] = dateFormat.format(receivedTimestamp)
|
map["receivedTimestamp"] = dateFormat.format(receivedTimestamp)
|
||||||
|
|
||||||
|
|
||||||
if (trainDisplay.isNotEmpty()) map["train"] = trainDisplay
|
trainDisplay?.takeIf { it.isNotEmpty() }?.let { map["train"] = it }
|
||||||
|
|
||||||
if (directionText != "未知") map["direction"] = directionText
|
if (directionText != "未知") map["direction"] = directionText
|
||||||
if (isValidValue(speed)) map["speed"] = "速度: ${speed.trim()} km/h"
|
if (isValidValue(speed)) map["speed"] = "${speed.trim()} km/h"
|
||||||
if (isValidValue(position)) map["position"] = "位置: ${position.trim()} km"
|
if (isValidValue(position)) {
|
||||||
|
map["position"] = "${position.trim().removeSuffix(".")} K"
|
||||||
|
}
|
||||||
val timeToDisplay = if (showDetailedTime) {
|
val timeToDisplay = if (showDetailedTime) {
|
||||||
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||||
if (isValidValue(time)) {
|
if (isValidValue(time)) {
|
||||||
"列车时间: $time\n接收时间: ${dateFormat.format(receivedTimestamp)}"
|
"$time\n${dateFormat.format(receivedTimestamp)}"
|
||||||
} else {
|
} else {
|
||||||
dateFormat.format(receivedTimestamp)
|
dateFormat.format(receivedTimestamp)
|
||||||
}
|
}
|
||||||
@@ -164,13 +181,13 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
map["time"] = timeToDisplay
|
map["time"] = timeToDisplay
|
||||||
if (isValidValue(loco)) map["loco"] = "机车号: ${loco.trim()}"
|
if (isValidValue(loco)) map["loco"] = "${loco.trim()}"
|
||||||
if (isValidValue(locoType)) map["loco_type"] = "型号: ${locoType.trim()}"
|
if (isValidValue(locoType)) map["loco_type"] = "${locoType.trim()}"
|
||||||
if (isValidValue(route)) map["route"] = "线路: ${route.trim()}"
|
if (isValidValue(route)) map["route"] = "${route.trim()}"
|
||||||
if (isValidValue(positionInfo) && !positionInfo.trim().matches(Regex(".*(<NUL>|\\s)*.*"))) {
|
if (isValidValue(positionInfo) && !positionInfo.trim().matches(Regex(".*(<NUL>|\\s)*.*"))) {
|
||||||
map["position_info"] = "位置信息: ${positionInfo.trim()}"
|
map["position_info"] = "${positionInfo.trim()}"
|
||||||
}
|
}
|
||||||
if (rssi != 0.0) map["rssi"] = "信号强度: $rssi dBm"
|
if (rssi != 0.0) map["rssi"] = "$rssi dBm"
|
||||||
|
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,12 +297,12 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun processRecordsForMerging(records: List<TrainRecord>, settings: MergeSettings): List<MergedTrainRecord> {
|
private fun processRecordsForMerging(records: List<TrainRecord>, settings: MergeSettings): List<MergedTrainRecord> {
|
||||||
|
val validRecords = settings.timeWindow.seconds?.let { windowSeconds ->
|
||||||
val currentTime = Date()
|
val currentTime = Date()
|
||||||
val validRecords = records.filter { record ->
|
records.filter { record ->
|
||||||
settings.timeWindow.seconds?.let { windowSeconds ->
|
|
||||||
(currentTime.time - record.timestamp.time) / 1000 <= windowSeconds
|
(currentTime.time - record.timestamp.time) / 1000 <= windowSeconds
|
||||||
} ?: true
|
|
||||||
}
|
}
|
||||||
|
} ?: records
|
||||||
|
|
||||||
return when (settings.groupBy) {
|
return when (settings.groupBy) {
|
||||||
GroupBy.TRAIN_OR_LOCO -> processTrainOrLocoMerging(validRecords)
|
GroupBy.TRAIN_OR_LOCO -> processTrainOrLocoMerging(validRecords)
|
||||||
@@ -317,11 +317,14 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
|
|
||||||
groupedRecords.mapNotNull { (groupKey, groupRecords) ->
|
groupedRecords.mapNotNull { (groupKey, groupRecords) ->
|
||||||
if (groupRecords.size >= 2) {
|
if (groupRecords.size >= 2) {
|
||||||
val sortedRecords = groupRecords.sortedBy { it.timestamp }
|
val latestRecord = if (groupRecords.size > 1) {
|
||||||
val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!!
|
groupRecords.maxByOrNull { it.timestamp } ?: groupRecords.last()
|
||||||
|
} else {
|
||||||
|
groupRecords.last()
|
||||||
|
}
|
||||||
MergedTrainRecord(
|
MergedTrainRecord(
|
||||||
groupKey = groupKey,
|
groupKey = groupKey,
|
||||||
records = sortedRecords,
|
records = groupRecords.toList(),
|
||||||
latestRecord = latestRecord
|
latestRecord = latestRecord
|
||||||
)
|
)
|
||||||
} else null
|
} else null
|
||||||
@@ -331,7 +334,9 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun processTrainOrLocoMerging(records: List<TrainRecord>): List<MergedTrainRecord> {
|
private fun processTrainOrLocoMerging(records: List<TrainRecord>): List<MergedTrainRecord> {
|
||||||
val groups = mutableListOf<MutableList<TrainRecord>>()
|
val trainGroups = mutableMapOf<String, MutableList<TrainRecord>>()
|
||||||
|
val locoGroups = mutableMapOf<String, MutableList<TrainRecord>>()
|
||||||
|
val mergedGroups = mutableSetOf<MutableList<TrainRecord>>()
|
||||||
|
|
||||||
records.forEach { record ->
|
records.forEach { record ->
|
||||||
val train = record.train.trim()
|
val train = record.train.trim()
|
||||||
@@ -341,38 +346,44 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
var foundGroup: MutableList<TrainRecord>? = null
|
var targetGroup: MutableList<TrainRecord>? = null
|
||||||
|
|
||||||
for (group in groups) {
|
if (train.isNotEmpty() && train != "<NUL>") {
|
||||||
val shouldMerge = group.any { existingRecord ->
|
targetGroup = trainGroups[train]
|
||||||
val existingTrain = existingRecord.train.trim()
|
|
||||||
val existingLoco = existingRecord.loco.trim()
|
|
||||||
|
|
||||||
(train.isNotEmpty() && train != "<NUL>" && train == existingTrain) ||
|
|
||||||
(loco.isNotEmpty() && loco != "<NUL>" && loco == existingLoco)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldMerge) {
|
if (targetGroup == null && loco.isNotEmpty() && loco != "<NUL>") {
|
||||||
foundGroup = group
|
targetGroup = locoGroups[loco]
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundGroup != null) {
|
if (targetGroup != null) {
|
||||||
foundGroup.add(record)
|
targetGroup.add(record)
|
||||||
|
if (train.isNotEmpty() && train != "<NUL>") {
|
||||||
|
trainGroups[train] = targetGroup
|
||||||
|
}
|
||||||
|
if (loco.isNotEmpty() && loco != "<NUL>") {
|
||||||
|
locoGroups[loco] = targetGroup
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
groups.add(mutableListOf(record))
|
val newGroup = mutableListOf(record)
|
||||||
|
mergedGroups.add(newGroup)
|
||||||
|
|
||||||
|
if (train.isNotEmpty() && train != "<NUL>") {
|
||||||
|
trainGroups[train] = newGroup
|
||||||
|
}
|
||||||
|
if (loco.isNotEmpty() && loco != "<NUL>") {
|
||||||
|
locoGroups[loco] = newGroup
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups.mapNotNull { groupRecords ->
|
return mergedGroups.mapNotNull { groupRecords ->
|
||||||
if (groupRecords.size >= 2) {
|
if (groupRecords.size >= 2) {
|
||||||
val sortedRecords = groupRecords.sortedBy { it.timestamp }
|
val latestRecord = groupRecords.maxByOrNull { it.timestamp } ?: groupRecords.lastOrNull() ?: return@mapNotNull null
|
||||||
val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!!
|
|
||||||
val groupKey = "${latestRecord.train}_OR_${latestRecord.loco}"
|
val groupKey = "${latestRecord.train}_OR_${latestRecord.loco}"
|
||||||
MergedTrainRecord(
|
MergedTrainRecord(
|
||||||
groupKey = groupKey,
|
groupKey = groupKey,
|
||||||
records = sortedRecords,
|
records = groupRecords.toList(),
|
||||||
latestRecord = latestRecord
|
latestRecord = latestRecord
|
||||||
)
|
)
|
||||||
} else null
|
} else null
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
package org.noxylva.lbjconsole.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
|
||||||
import org.osmdroid.views.MapView
|
|
||||||
import org.osmdroid.views.overlay.Marker
|
|
||||||
import org.noxylva.lbjconsole.model.TrainRecord
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TrainDetailDialog(
|
|
||||||
trainRecord: TrainRecord,
|
|
||||||
onDismiss: () -> Unit
|
|
||||||
) {
|
|
||||||
val recordMap = trainRecord.toMap()
|
|
||||||
val coordinates = remember { trainRecord.getCoordinates() }
|
|
||||||
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
properties = DialogProperties(
|
|
||||||
dismissOnBackPress = true,
|
|
||||||
dismissOnClickOutside = true
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "列车详情",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
DetailItem("列车号", recordMap["train"] ?: "--")
|
|
||||||
DetailItem("方向", recordMap["direction"] ?: "未知")
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
DetailItem("接收时间", recordMap["timestamp"] ?: "--")
|
|
||||||
DetailItem("列车时间", recordMap["time"] ?: "--")
|
|
||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
DetailItem("速度", recordMap["speed"] ?: "--")
|
|
||||||
DetailItem("位置", recordMap["position"] ?: "--")
|
|
||||||
DetailItem("位置信息", recordMap["position_info"] ?: "--")
|
|
||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
DetailItem("机车号", recordMap["loco"] ?: "--")
|
|
||||||
DetailItem("机车类型", recordMap["loco_type"] ?: "--")
|
|
||||||
DetailItem("列车类型", recordMap["lbj_class"] ?: "--")
|
|
||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
DetailItem("路线", recordMap["route"] ?: "--")
|
|
||||||
DetailItem("信号强度", recordMap["rssi"] ?: "--")
|
|
||||||
|
|
||||||
if (coordinates != null) {
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
DetailItem(
|
|
||||||
label = "经纬度",
|
|
||||||
value = "纬度: ${coordinates.latitude}, 经度: ${coordinates.longitude}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(200.dp)
|
|
||||||
.padding(vertical = 8.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
AndroidView(
|
|
||||||
factory = { context ->
|
|
||||||
MapView(context).apply {
|
|
||||||
setTileSource(TileSourceFactory.MAPNIK)
|
|
||||||
setMultiTouchControls(true)
|
|
||||||
controller.setZoom(15.0)
|
|
||||||
controller.setCenter(coordinates)
|
|
||||||
|
|
||||||
|
|
||||||
val marker = Marker(this)
|
|
||||||
marker.position = coordinates
|
|
||||||
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
|
||||||
marker.title = recordMap["train"] ?: "列车"
|
|
||||||
overlays.add(marker)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update = { mapView ->
|
|
||||||
mapView.controller.setCenter(coordinates)
|
|
||||||
mapView.invalidate()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = onDismiss,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
) {
|
|
||||||
Text("关闭")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DetailItem(
|
|
||||||
label: String,
|
|
||||||
value: String,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = value,
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -155,25 +155,38 @@ fun TrainRecordItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
val hasTrainDisplay = recordMap["train"]?.toString()?.isNotEmpty() ?: false
|
||||||
|
val hasRouteOrPosition = record.route.trim().isNotEmpty() && !record.route.trim().all { it == '*' } ||
|
||||||
|
record.position.trim().isNotEmpty() && !record.position.trim().all { it == '-' || it == '.' } && record.position.trim() != "<NUL>"
|
||||||
|
val hasSpeed = record.speed.trim().isNotEmpty() &&
|
||||||
|
!record.speed.trim().all { it == '*' || it == '-' } &&
|
||||||
|
record.speed.trim() != "NUL" && record.speed.trim() != "<NUL>"
|
||||||
|
val hasLocoInfo = locoInfoUtil != null && record.locoType.isNotEmpty() && record.loco.isNotEmpty() &&
|
||||||
|
locoInfoUtil.getLocoInfoDisplay(record.locoType, record.loco) != null
|
||||||
|
|
||||||
|
val shouldShowOnlyTime = !hasTrainDisplay && !hasRouteOrPosition && !hasSpeed && !hasLocoInfo
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(if (shouldShowOnlyTime) 0.dp else 2.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
val trainDisplay = recordMap["train"]?.toString() ?: "未知列车"
|
val trainDisplay = recordMap["train"]?.toString() ?: ""
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
|
if (trainDisplay.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = trainDisplay,
|
text = trainDisplay,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val directionText = when (record.direction) {
|
val directionText = when (record.direction) {
|
||||||
1 -> "下"
|
1 -> "下"
|
||||||
@@ -227,7 +240,7 @@ fun TrainRecordItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(if (shouldShowOnlyTime) 0.dp else 2.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -258,7 +271,7 @@ fun TrainRecordItem(
|
|||||||
|
|
||||||
if (isValidPosition) {
|
if (isValidPosition) {
|
||||||
Text(
|
Text(
|
||||||
text = "${position}K",
|
text = "${position.trim().removeSuffix(".")}K",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
modifier = Modifier.alignByBaseline()
|
modifier = Modifier.alignByBaseline()
|
||||||
@@ -287,7 +300,7 @@ fun TrainRecordItem(
|
|||||||
record.loco
|
record.loco
|
||||||
)
|
)
|
||||||
if (locoInfoText != null) {
|
if (locoInfoText != null) {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(if (shouldShowOnlyTime) 0.dp else 2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = locoInfoText,
|
text = locoInfoText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
@@ -295,7 +308,8 @@ fun TrainRecordItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
Spacer(modifier = Modifier.height(if (shouldShowOnlyTime) 0.dp else 2.dp))
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isExpanded,
|
visible = isExpanded,
|
||||||
enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)),
|
enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)),
|
||||||
@@ -528,18 +542,20 @@ fun MergedTrainRecordItem(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
val trainDisplay = recordMap["train"]?.toString() ?: "未知列车"
|
val trainDisplay = recordMap["train"]?.toString() ?: ""
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
|
if (trainDisplay.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = trainDisplay,
|
text = trainDisplay,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val directionText = when (latestRecord.direction) {
|
val directionText = when (latestRecord.direction) {
|
||||||
1 -> "下"
|
1 -> "下"
|
||||||
@@ -593,7 +609,7 @@ fun MergedTrainRecordItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -624,7 +640,7 @@ fun MergedTrainRecordItem(
|
|||||||
|
|
||||||
if (isValidPosition) {
|
if (isValidPosition) {
|
||||||
Text(
|
Text(
|
||||||
text = "${position}K",
|
text = "${position.trim().removeSuffix(".")}K",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
modifier = Modifier.alignByBaseline()
|
modifier = Modifier.alignByBaseline()
|
||||||
@@ -653,7 +669,7 @@ fun MergedTrainRecordItem(
|
|||||||
latestRecord.loco
|
latestRecord.loco
|
||||||
)
|
)
|
||||||
if (locoInfoText != null) {
|
if (locoInfoText != null) {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = locoInfoText,
|
text = locoInfoText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
@@ -661,7 +677,7 @@ fun MergedTrainRecordItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isExpanded,
|
visible = isExpanded,
|
||||||
enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)),
|
enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)),
|
||||||
@@ -782,14 +798,14 @@ fun MergedTrainRecordItem(
|
|||||||
controller.setZoom(mapViewState.zoom)
|
controller.setZoom(mapViewState.zoom)
|
||||||
controller.setCenter(mapViewState.center)
|
controller.setCenter(mapViewState.center)
|
||||||
} else if (allValidCoordinates.size > 1) {
|
} else if (allValidCoordinates.size > 1) {
|
||||||
val boundingBox = BoundingBox.fromGeoPoints(allValidCoordinates)
|
val boundingBox = BoundingBox.fromGeoPoints(allValidCoordinates.filter { it.latitude != 0.0 || it.longitude != 0.0 })
|
||||||
val layoutListener = object : android.view.View.OnLayoutChangeListener {
|
val layoutListener = object : android.view.View.OnLayoutChangeListener {
|
||||||
override fun onLayoutChange(v: android.view.View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
override fun onLayoutChange(v: android.view.View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
||||||
if (width > 0 && height > 0) {
|
if (width > 0 && height > 0) {
|
||||||
val zoomLevel = org.osmdroid.views.MapView.getTileSystem().getBoundingBoxZoom(boundingBox, width, height)
|
val zoomLevel = org.osmdroid.views.MapView.getTileSystem().getBoundingBoxZoom(boundingBox, width, height)
|
||||||
val latSpan = boundingBox.latitudeSpan
|
val latSpan = boundingBox.latitudeSpan
|
||||||
val adjustedCenter = org.osmdroid.util.GeoPoint(
|
val adjustedCenter = org.osmdroid.util.GeoPoint(
|
||||||
boundingBox.center.latitude + latSpan * 0.25, // Shift center UP (north) to create top padding
|
boundingBox.center.latitude + latSpan * 0.25,
|
||||||
boundingBox.center.longitude
|
boundingBox.center.longitude
|
||||||
)
|
)
|
||||||
val newZoom = zoomLevel - 1.0
|
val newZoom = zoomLevel - 1.0
|
||||||
@@ -805,7 +821,7 @@ fun MergedTrainRecordItem(
|
|||||||
addOnLayoutChangeListener(layoutListener)
|
addOnLayoutChangeListener(layoutListener)
|
||||||
} else if (allValidCoordinates.isNotEmpty()) {
|
} else if (allValidCoordinates.isNotEmpty()) {
|
||||||
val center = allValidCoordinates.first()
|
val center = allValidCoordinates.first()
|
||||||
val zoom = 14.0
|
val zoom = 10.0
|
||||||
controller.setZoom(zoom)
|
controller.setZoom(zoom)
|
||||||
controller.setCenter(center)
|
controller.setCenter(center)
|
||||||
onMapViewStateChange(CardMapView(center, zoom))
|
onMapViewStateChange(CardMapView(center, zoom))
|
||||||
@@ -907,12 +923,12 @@ fun MergedTrainRecordItem(
|
|||||||
}
|
}
|
||||||
if (recordItem.position.isNotEmpty() && recordItem.position != "<NUL>") {
|
if (recordItem.position.isNotEmpty() && recordItem.position != "<NUL>") {
|
||||||
if (isNotEmpty()) append(" ")
|
if (isNotEmpty()) append(" ")
|
||||||
append("${recordItem.position}K")
|
append("${recordItem.position.trim().removeSuffix(".")}K")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = locationText.ifEmpty { "位置未知" },
|
text = locationText.ifEmpty { "" },
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ import android.graphics.PorterDuff
|
|||||||
import android.graphics.PorterDuffColorFilter
|
import android.graphics.PorterDuffColorFilter
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.MyLocation
|
import androidx.compose.material.icons.filled.MyLocation
|
||||||
@@ -16,11 +21,16 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -112,7 +122,6 @@ fun MapScreen(
|
|||||||
|
|
||||||
val recordMap = record.toMap()
|
val recordMap = record.toMap()
|
||||||
title = recordMap["train"]?.toString() ?: "列车"
|
title = recordMap["train"]?.toString() ?: "列车"
|
||||||
|
|
||||||
val latStr = String.format("%.4f", point.latitude)
|
val latStr = String.format("%.4f", point.latitude)
|
||||||
val lonStr = String.format("%.4f", point.longitude)
|
val lonStr = String.format("%.4f", point.longitude)
|
||||||
val coordStr = "${latStr}°N, ${lonStr}°E"
|
val coordStr = "${latStr}°N, ${lonStr}°E"
|
||||||
@@ -574,8 +583,8 @@ fun Context.getCompactMarkerDrawable(color: Int): Drawable {
|
|||||||
|
|
||||||
|
|
||||||
private fun Int.directionText(): String = when (this) {
|
private fun Int.directionText(): String = when (this) {
|
||||||
1 -> "↓"
|
1 -> "下行"
|
||||||
3 -> "↑"
|
3 -> "上行"
|
||||||
else -> "?"
|
else -> "?"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,50 +594,143 @@ private fun TrainMarkerDialog(
|
|||||||
position: GeoPoint?,
|
position: GeoPoint?,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val recordMap = record.toMap()
|
||||||
|
|
||||||
|
val displayItems = recordMap.filterKeys {
|
||||||
|
it !in setOf("train", "direction", "time")
|
||||||
|
}.toList()
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = {
|
title = {
|
||||||
|
|
||||||
val recordMap = record.toMap()
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(text = recordMap["train"]?.toString() ?: "列车", style = MaterialTheme.typography.titleLarge)
|
|
||||||
recordMap["direction"]?.let { direction ->
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = direction,
|
text = recordMap["train"]?.toString() ?: "列车",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
recordMap["direction"]?.let { direction ->
|
||||||
|
Text(
|
||||||
|
text = (direction as? Int)?.directionText() ?: direction.toString(),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontSize = 12.sp
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
displayItems.forEach { (key, value) ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
val title = when (key) {
|
||||||
|
"speed" -> "速度"
|
||||||
|
"position" -> "位置"
|
||||||
|
"time" -> "时间"
|
||||||
|
"loco" -> "机车号"
|
||||||
|
"loco_type" -> "机车型号"
|
||||||
|
"route" -> "线路"
|
||||||
|
"rssi" -> "信号强度"
|
||||||
|
"timestamp" -> "时间"
|
||||||
|
"receivedTimestamp" -> "接收时间"
|
||||||
|
else -> key
|
||||||
|
}
|
||||||
|
|
||||||
record.toMap().forEach { (key, value) ->
|
|
||||||
if (key != "train" && key != "direction") {
|
|
||||||
Text(
|
Text(
|
||||||
text = value,
|
text = title,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.titleMedium
|
||||||
modifier = Modifier.padding(vertical = 2.dp)
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
position?.let {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = "坐标: ${String.format("%.6f", it.latitude)}, ${String.format("%.6f", it.longitude)}",
|
text = value.toString(),
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
position?.let {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "坐标信息",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${String.format("%.6f", it.latitude)}, ${String.format("%.6f", it.longitude)}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(onClick = onDismiss) {
|
||||||
Text("确定")
|
Text("关闭")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoSection(title: String, items: List<Pair<String, String>>) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
|
)
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
items.forEach { (key, value) ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 2.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = value.toString(),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoSectionSimple(title: String, items: List<Pair<String, String>>) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
items.forEach { (key, value) ->
|
||||||
|
Text(
|
||||||
|
text = value.toString(),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
package org.noxylva.lbjconsole.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.animation.core.*
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import org.noxylva.lbjconsole.model.TrainRecord
|
|
||||||
import org.noxylva.lbjconsole.ui.components.TrainDetailDialog
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MonitorScreen(
|
|
||||||
latestRecord: TrainRecord?,
|
|
||||||
recentRecords: List<TrainRecord>,
|
|
||||||
lastUpdateTime: Date?,
|
|
||||||
temporaryStatusMessage: String? = null,
|
|
||||||
onRecordClick: (TrainRecord) -> Unit,
|
|
||||||
onClearLog: () -> Unit
|
|
||||||
) {
|
|
||||||
var showDetailDialog by remember { mutableStateOf(false) }
|
|
||||||
var selectedRecord by remember { mutableStateOf<TrainRecord?>(null) }
|
|
||||||
var isPressed by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val scale by animateFloatAsState(
|
|
||||||
targetValue = if (isPressed) 0.98f else 1f,
|
|
||||||
animationSpec = tween(durationMillis = 120),
|
|
||||||
label = "content_scale"
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(isPressed) {
|
|
||||||
if (isPressed) {
|
|
||||||
delay(100)
|
|
||||||
isPressed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val timeSinceLastUpdate = remember { mutableStateOf<String?>(null) }
|
|
||||||
LaunchedEffect(key1 = lastUpdateTime) {
|
|
||||||
if (lastUpdateTime != null) {
|
|
||||||
while (true) {
|
|
||||||
val now = Date()
|
|
||||||
val diffInSec = (now.time - lastUpdateTime.time) / 1000
|
|
||||||
timeSinceLastUpdate.value = when {
|
|
||||||
diffInSec < 60 -> "${diffInSec}秒前"
|
|
||||||
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
|
|
||||||
else -> "${diffInSec / 3600}小时前"
|
|
||||||
}
|
|
||||||
val updateInterval = if (diffInSec < 60) 500L else if (diffInSec < 3600) 30000L else 300000L
|
|
||||||
delay(updateInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
|
||||||
Card(modifier = Modifier.fillMaxSize()) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(20.dp)
|
|
||||||
) {
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = timeSinceLastUpdate.value ?: "暂无数据",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = latestRecord,
|
|
||||||
transitionSpec = {
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 300,
|
|
||||||
easing = FastOutSlowInEasing
|
|
||||||
)
|
|
||||||
) + slideInVertically(
|
|
||||||
initialOffsetY = { it / 4 },
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 300,
|
|
||||||
easing = FastOutSlowInEasing
|
|
||||||
)
|
|
||||||
) togetherWith fadeOut(
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 150,
|
|
||||||
easing = FastOutLinearInEasing
|
|
||||||
)
|
|
||||||
) + slideOutVertically(
|
|
||||||
targetOffsetY = { -it / 4 },
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 150,
|
|
||||||
easing = FastOutLinearInEasing
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = "content_animation"
|
|
||||||
) { record ->
|
|
||||||
if (record != null) {
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = rememberRipple(bounded = true)
|
|
||||||
) {
|
|
||||||
isPressed = true
|
|
||||||
selectedRecord = record
|
|
||||||
showDetailDialog = true
|
|
||||||
onRecordClick(record)
|
|
||||||
}
|
|
||||||
.padding(8.dp)
|
|
||||||
.graphicsLayer {
|
|
||||||
scaleX = scale
|
|
||||||
scaleY = scale
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
|
|
||||||
val recordMap = record.toMap()
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = recordMap["train"]?.toString() ?: "",
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = recordMap["direction"]?.toString() ?: "",
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = when(recordMap["direction"]?.toString()) {
|
|
||||||
"上行" -> MaterialTheme.colorScheme.primary
|
|
||||||
"下行" -> MaterialTheme.colorScheme.secondary
|
|
||||||
else -> MaterialTheme.colorScheme.onSurface
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
|
|
||||||
|
|
||||||
if (recordMap.containsKey("time")) {
|
|
||||||
recordMap["time"]?.split("\n")?.forEach { timeLine ->
|
|
||||||
Text(
|
|
||||||
text = timeLine,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(thickness = 0.5.dp)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
recordMap["speed"]?.let { speed ->
|
|
||||||
Text(
|
|
||||||
text = speed,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
recordMap["position"]?.let { position ->
|
|
||||||
Text(
|
|
||||||
text = position,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
recordMap.forEach { (key, value) ->
|
|
||||||
when (key) {
|
|
||||||
"timestamp", "train", "direction", "time", "speed", "position", "position_info" -> {}
|
|
||||||
else -> {
|
|
||||||
Text(
|
|
||||||
text = value,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (recordMap.containsKey("position_info")) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = recordMap["position_info"] ?: "",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text(
|
|
||||||
"暂无列车信息",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
color = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
|
|
||||||
if (lastUpdateTime != null) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
"上次接收数据: ${SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(lastUpdateTime)}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (showDetailDialog && selectedRecord != null) {
|
|
||||||
TrainDetailDialog(
|
|
||||||
trainRecord = selectedRecord!!,
|
|
||||||
onDismiss = { showDetailDialog = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun InfoItem(
|
|
||||||
label: String,
|
|
||||||
value: String,
|
|
||||||
fontSize: TextUnit = 14.sp
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 2.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "$label: ",
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
fontSize = fontSize,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = value,
|
|
||||||
fontSize = fontSize,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.noxylva.lbjconsole.ui.screens
|
package org.noxylva.lbjconsole.ui.screens
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -19,9 +20,10 @@ import kotlinx.coroutines.launch
|
|||||||
import org.noxylva.lbjconsole.model.MergeSettings
|
import org.noxylva.lbjconsole.model.MergeSettings
|
||||||
import org.noxylva.lbjconsole.model.GroupBy
|
import org.noxylva.lbjconsole.model.GroupBy
|
||||||
import org.noxylva.lbjconsole.model.TimeWindow
|
import org.noxylva.lbjconsole.model.TimeWindow
|
||||||
import org.noxylva.lbjconsole.SettingsActivity
|
import org.noxylva.lbjconsole.database.AppSettingsRepository
|
||||||
import org.noxylva.lbjconsole.BackgroundService
|
import org.noxylva.lbjconsole.BackgroundService
|
||||||
import org.noxylva.lbjconsole.NotificationService
|
import org.noxylva.lbjconsole.NotificationService
|
||||||
|
import org.noxylva.lbjconsole.FilePickerActivity
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -199,7 +201,8 @@ fun SettingsScreen(
|
|||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(context) {
|
LaunchedEffect(context) {
|
||||||
backgroundServiceEnabled = SettingsActivity.isBackgroundServiceEnabled(context)
|
val repository = AppSettingsRepository(context)
|
||||||
|
backgroundServiceEnabled = repository.getSettings().backgroundServiceEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
var notificationEnabled by remember(context, notificationService) {
|
var notificationEnabled by remember(context, notificationService) {
|
||||||
@@ -231,7 +234,8 @@ fun SettingsScreen(
|
|||||||
onCheckedChange = { enabled ->
|
onCheckedChange = { enabled ->
|
||||||
backgroundServiceEnabled = enabled
|
backgroundServiceEnabled = enabled
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
SettingsActivity.setBackgroundServiceEnabled(context, enabled)
|
val repository = AppSettingsRepository(context)
|
||||||
|
repository.updateBackgroundServiceEnabled(enabled)
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
BackgroundService.startService(context)
|
BackgroundService.startService(context)
|
||||||
} else {
|
} else {
|
||||||
@@ -424,6 +428,66 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Storage,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"数据管理",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val intent = FilePickerActivity.createExportIntent(context)
|
||||||
|
context.startActivity(intent)
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f).padding(horizontal = 4.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Upload, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("导出")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val intent = FilePickerActivity.createImportIntent(context)
|
||||||
|
context.startActivity(intent)
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f).padding(horizontal = 4.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Download, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("导入")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "LBJ Console v$appVersion by undef-i",
|
text = "LBJ Console v$appVersion by undef-i",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package org.noxylva.lbjconsole.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.noxylva.lbjconsole.database.AppSettingsEntity
|
||||||
|
import org.noxylva.lbjconsole.database.TrainDatabase
|
||||||
|
import org.noxylva.lbjconsole.database.TrainRecordEntity
|
||||||
|
import java.io.*
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
class DatabaseExportImportUtil(private val context: Context) {
|
||||||
|
private val gson = Gson()
|
||||||
|
private val database = TrainDatabase.getDatabase(context)
|
||||||
|
|
||||||
|
data class SimpleRecordBackup(
|
||||||
|
val records: List<TrainRecordEntity>
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun exportDatabase(): String = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val trainRecords = database.trainRecordDao().getAllRecords()
|
||||||
|
|
||||||
|
val backup = SimpleRecordBackup(
|
||||||
|
records = trainRecords
|
||||||
|
)
|
||||||
|
|
||||||
|
val json = gson.toJson(backup)
|
||||||
|
json
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("DatabaseExport", "导出失败", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun importDatabase(uri: Uri): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
val json = inputStream.bufferedReader().use { it.readText() }
|
||||||
|
val backup: SimpleRecordBackup = gson.fromJson(json, object : TypeToken<SimpleRecordBackup>() {}.type)
|
||||||
|
|
||||||
|
database.trainRecordDao().deleteAllRecords()
|
||||||
|
|
||||||
|
if (backup.records.isNotEmpty()) {
|
||||||
|
database.trainRecordDao().insertRecords(backup.records)
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
} ?: false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("DatabaseImport", "导入失败", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getExportFileUri(): Uri {
|
||||||
|
val filePath = exportDatabase()
|
||||||
|
return Uri.parse("file://$filePath")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,8 @@ import android.util.Log
|
|||||||
import org.osmdroid.util.GeoPoint
|
import org.osmdroid.util.GeoPoint
|
||||||
|
|
||||||
|
|
||||||
object LocationUtils {
|
object LocationUtil {
|
||||||
private const val TAG = "LocationUtils"
|
private const val TAG = "LocationUtil"
|
||||||
|
|
||||||
|
|
||||||
fun parsePositionInfo(positionInfo: String): GeoPoint? {
|
fun parsePositionInfo(positionInfo: String): GeoPoint? {
|
||||||
@@ -52,7 +52,7 @@ object LocationUtils {
|
|||||||
|
|
||||||
val minuteEndIndex = dmsString.indexOf('′')
|
val minuteEndIndex = dmsString.indexOf('′')
|
||||||
if (minuteEndIndex == -1) {
|
if (minuteEndIndex == -1) {
|
||||||
return degrees
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val minutes = dmsString.substring(degreeIndex + 1, minuteEndIndex).toDouble()
|
val minutes = dmsString.substring(degreeIndex + 1, minuteEndIndex).toDouble()
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.noxylva.lbjconsole.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
|
class LocoTypeUtil(private val context: Context) {
|
||||||
|
private val locoTypeMap = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadLocoTypeMapping()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadLocoTypeMapping() {
|
||||||
|
try {
|
||||||
|
context.assets.open("loco_type_info.csv").use { inputStream ->
|
||||||
|
BufferedReader(InputStreamReader(inputStream)).use { reader ->
|
||||||
|
reader.lines().forEach { line ->
|
||||||
|
val parts = line.split(",")
|
||||||
|
if (parts.size >= 2) {
|
||||||
|
val code = parts[0].trim()
|
||||||
|
val type = parts[1].trim()
|
||||||
|
locoTypeMap[code] = type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocoTypeByCode(code: String): String? {
|
||||||
|
return locoTypeMap[code]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocoTypeByLocoNumber(locoNumber: String): String? {
|
||||||
|
if (locoNumber.length < 3) return null
|
||||||
|
val prefix = locoNumber.take(3)
|
||||||
|
return getLocoTypeByCode(prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M12,6m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M12,10L12,10c-2.2,0 -4,1.8 -4,4v6h8v-6C16,11.8 14.2,10 12,10z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:background="?android:attr/selectableItemBackground"
|
|
||||||
android:clickable="true">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Background Service"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textColor="@android:color/black"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Keep app running in background"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textColor="@android:color/darker_gray"
|
|
||||||
android:layout_marginTop="4dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<Switch
|
|
||||||
android:id="@+id/switch_background_service"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="@android:color/darker_gray"
|
|
||||||
android:layout_marginHorizontal="16dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,590 +0,0 @@
|
|||||||
6G,51,90,西安铁路局 宝鸡电力机务段,,
|
|
||||||
6K,1,85,中国铁路郑州局集团有限公司 洛阳机务段,,
|
|
||||||
8G,1,1,中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段,,
|
|
||||||
8G,2,2,中国铁道博物馆,,
|
|
||||||
8G,3,75,中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段,,
|
|
||||||
8G,76,76,中国铁路太原局集团有限公司 太原机务段北场,,
|
|
||||||
8G,77,96,中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段,,
|
|
||||||
8G,97,97,中国铁路太原局集团有限公司 榆次机务折返段,,
|
|
||||||
8G,98,100,中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段,,
|
|
||||||
8K,1,1,中国铁路北京局集团有限公司 丰台机务段,,
|
|
||||||
8K,2,7,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
|
|
||||||
8K,8,8,中国铁道博物馆,*科技号,
|
|
||||||
8K,9,17,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
|
|
||||||
8K,18,18,*北京铁路局 丰台机务段,,
|
|
||||||
8K,19,23,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
|
|
||||||
8K,24,24,中国铁路太原局集团有限公司 湖东机务段 大同西运用车间,,
|
|
||||||
8K,25,64,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
|
|
||||||
8K,65,65,天津铁道职业技术学院,,
|
|
||||||
8K,66,71,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
|
|
||||||
8K,72,72,中国铁路北京局集团有限公司 丰台机务段,,
|
|
||||||
8K,73,90,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
|
|
||||||
8K,91,91,中国铁路太原局集团有限公司 太原机务段北场 机车展场,,
|
|
||||||
8K,92,100,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
|
|
||||||
CR400AF,21,21,中国铁路北京局集团有限公司 朝阳动车运用所,CR400AF-G,
|
|
||||||
CR400AF,207,208,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,1001,1002,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,1003,1003,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
|
|
||||||
CR400AF,1004,1004,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,1005,1005,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
|
|
||||||
CR400AF,1006,1006,中国铁路广州局集团有限公司 广州南动车运用所,,
|
|
||||||
CR400AF,1007,1009,中国铁路广州局集团有限公司 潮州动车运用所,,
|
|
||||||
CR400AF,1010,1010,中国铁路广州局集团有限公司 广州南动车运用所,,
|
|
||||||
CR400AF,1011,1014,中国铁路广州局集团有限公司 潮州动车运用所,,
|
|
||||||
CR400AF,1015,1020,中国铁路广州局集团有限公司 广州南动车运用所,,
|
|
||||||
CR400AF,1021,1021,中国铁路广州局集团有限公司 潮州动车运用所,,
|
|
||||||
CR400AF,1022,1025,中国铁路广州局集团有限公司 广州南动车运用所,,
|
|
||||||
CR400AF,1026,1027,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
|
|
||||||
CR400AF,1028,1029,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,1030,1030,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
|
|
||||||
CR400AF,1031,1031,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,1032,1032,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
|
|
||||||
CR400AF,1033,1033,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,1034,1038,中国铁路广州局集团有限公司 广州南动车运用所,CR400AF-A,
|
|
||||||
CR400AF,1039,1039,中国铁路广州局集团有限公司 潮州动车运用所,,
|
|
||||||
CR400AF,1040,1040,中国铁路广州局集团有限公司 广州南动车运用所,,
|
|
||||||
CR400AF,2002,2002,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2004,2004,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2005,2005,中国铁路成都局集团有限公司 重庆西动车运用所,,
|
|
||||||
CR400AF,2006,2007,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2008,2008,中国铁路北京局集团有限公司 雄安动车运用所,,
|
|
||||||
CR400AF,2009,2010,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2011,2011,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2012,2012,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2013,2013,中国铁路成都局集团有限公司 重庆西动车运用所,,
|
|
||||||
CR400AF,2014,2016,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2017,2017,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2023,2023,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2024,2024,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2025,2025,中国铁路成都局集团有限公司 重庆西动车运用所,,
|
|
||||||
CR400AF,2026,2028,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2030,2030,中国铁路北京局集团有限公司 雄安动车运用所,,
|
|
||||||
CR400AF,2031,2032,中国铁路成都局集团有限公司 重庆西动车运用所,,
|
|
||||||
CR400AF,2033,2033,中国铁路北京局集团有限公司 雄安动车运用所,,
|
|
||||||
CR400AF,2034,2034,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2035,2038,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2040,2046,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2047,2048,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2049,2049,中国铁路成都局集团有限公司 重庆西动车运用所,,
|
|
||||||
CR400AF,2051,2051,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2053,2055,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2057,2057,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2058,2058,中国铁路北京局集团有限公司 雄安动车运用所,,
|
|
||||||
CR400AF,2060,2062,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2064,2064,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2065,2066,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2067,2068,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2069,2069,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2070,2070,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2071,2071,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2072,2072,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2073,2073,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2074,2076,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2077,2079,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2080,2084,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2085,2085,中国铁路济南局集团有限公司 济南东动车运用所,,
|
|
||||||
CR400AF,2086,2086,中国铁路济南局集团有限公司 青岛动车运用所,,
|
|
||||||
CR400AF,2087,2087,中国铁路济南局集团有限公司 济南东动车运用所,,
|
|
||||||
CR400AF,2088,2090,中国铁路济南局集团有限公司 青岛动车运用所,,
|
|
||||||
CR400AF,2091,2094,中国铁路济南局集团有限公司 济南东动车运用所,,
|
|
||||||
CR400AF,2095,2097,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2098,2098,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2099,2100,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2102,2102,中国铁路济南局集团有限公司 济南东动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2103,2104,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2105,2105,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2106,2106,中国铁路广州局集团有限公司 长沙动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2107,2115,中国铁路济南局集团有限公司 济南东动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2116,2123,中国铁路北京局集团有限公司 北京南动车运用所,CR400AF-B,
|
|
||||||
CR400AF,2124,2124,中国铁路武汉局集团有限公司 武汉动车运用所,,
|
|
||||||
CR400AF,2125,2125,中国铁路武汉局集团有限公司 武汉动车运用所,,
|
|
||||||
CR400AF,2126,2127,中国铁路武汉局集团有限公司 汉口动车运用所,,
|
|
||||||
CR400AF,2128,2128,中国铁路武汉局集团有限公司 武汉动车运用所,,
|
|
||||||
CR400AF,2130,2131,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2133,2133,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2134,2134,中国铁路济南局集团有限公司 青岛动车运用所,,
|
|
||||||
CR400AF,2135,2135,中国铁路成都局集团有限公司 重庆西动车运用所,,
|
|
||||||
CR400AF,2136,2138,中国铁路济南局集团有限公司 青岛动车运用所,,
|
|
||||||
CR400AF,2139,2139,中国铁路济南局集团有限公司 济南东动车运用所,,
|
|
||||||
CR400AF,2140,2140,中国铁路成都局集团有限公司 重庆西动车运用所,,
|
|
||||||
CR400AF,2141,2141,中国铁路济南局集团有限公司 济南东动车运用所,,
|
|
||||||
CR400AF,2142,2144,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2145,2146,中国铁路北京局集团有限公司 雄安动车运用所,,
|
|
||||||
CR400AF,2148,2150,中国铁路武汉局集团有限公司 汉口动车运用所,,
|
|
||||||
CR400AF,2151,2151,中国铁路武汉局集团有限公司 武汉动车运用所,,
|
|
||||||
CR400AF,2152,2153,中国铁路武汉局集团有限公司 汉口动车运用所,,
|
|
||||||
CR400AF,2154,2156,中国铁路武汉局集团有限公司 武汉动车运用所,,
|
|
||||||
CR400AF,2159,2159,中国铁路武汉局集团有限公司 武汉动车运用所,,
|
|
||||||
CR400AF,2160,2161,中国铁路武汉局集团有限公司 汉口动车运用所,,
|
|
||||||
CR400AF,2162,2163,中国铁路济南局集团有限公司 济南东动车运用所,,
|
|
||||||
CR400AF,2164,2164,中国铁路武汉局集团有限公司 武汉动车运用所,,
|
|
||||||
CR400AF,2171,2172,中国铁路武汉局集团有限公司 武汉动车运用所,,
|
|
||||||
CR400AF,2173,2173,中国铁路武汉局集团有限公司 汉口动车运用所,,
|
|
||||||
CR400AF,2174,2177,中国铁路武汉局集团有限公司 武汉动车运用所,,
|
|
||||||
CR400AF,2178,2178,中国铁路北京局集团有限公司 雄安动车运用所,,
|
|
||||||
CR400AF,2179,2179,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2180,2180,中国铁路北京局集团有限公司 雄安动车运用所,,
|
|
||||||
CR400AF,2181,2182,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2183,2187,中国铁路北京局集团有限公司 北京南动车运用所,,
|
|
||||||
CR400AF,2190,2192,中国铁路武汉局集团有限公司 武汉动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2193,2193,中国铁路济南局集团有限公司 济南东动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2194,2195,中国铁路广州局集团有限公司 深圳动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2196,2200,中国铁路武汉局集团有限公司 武汉动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2201,2205,中国铁路济南局集团有限公司 济南东动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2206,2210,中国铁路北京局集团有限公司 北京南动车运用所,CR400AF-B,
|
|
||||||
CR400AF,2211,2212,中国铁路济南局集团有限公司 济南东动车运用所,CR400AF-A,
|
|
||||||
CR400AF,2213,2213,中国铁路北京局集团有限公司 北京西动车运用所,,
|
|
||||||
CR400AF,2215,2217,中国铁路北京局集团有限公司 朝阳动车运用所,CR400AF-G,
|
|
||||||
CR400AF,2222,2225,中国铁路上海局集团有限公司 上海南动车运用所,,
|
|
||||||
CR400AF,2226,2226,中国铁路广州局集团有限公司 广州南动车运用所,,
|
|
||||||
CR400AF,2227,2227,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2228,2228,中国铁路广州局集团有限公司 广州南动车运用所,,
|
|
||||||
CR400AF,2229,2229,中国铁路广州局集团有限公司 长沙动车运用所,,
|
|
||||||
CR400AF,2230,2231,中国铁路济南局集团有限公司 济南东动车运用所,,
|
|
||||||
CR400AF,2232,2235,中国铁路上海局集团有限公司 上海南动车运用所,,
|
|
||||||
CR400AF,2236,2236,中国铁路成都局集团有限公司 重庆西动车运用所,,
|
|
||||||
CR400AF,2237,2243,中国铁路上海局集团有限公司 上海南动车运用所,,
|
|
||||||
CR400AF,2244,2248,中国铁路成都局集团有限公司 重庆西动车运用所,,
|
|
||||||
CR400AF,2254,2256,中国铁路成都局集团有限公司 重庆西动车运用所,,
|
|
||||||
DJ1,1,1,中国铁道科学研究院 环形铁道,,
|
|
||||||
DJ1,2,2,株洲西门子牵引设备有限公司,,
|
|
||||||
DJ1,3,3,西安铁路局 宝鸡机务段 秦岭附加队 ,,
|
|
||||||
DJ2,1,1,中国铁路郑州局集团有限公司 郑州机务段京武快车队,奥星,
|
|
||||||
DJ2,2,3,中国铁路郑州局集团有限公司 郑州机务段,奥星,
|
|
||||||
HXD1D,1,15,中国铁路武汉局集团有限公司 武昌南机务段,,
|
|
||||||
HXD1D,16,16,中国铁路上海局集团有限公司 杭州机务段,,
|
|
||||||
HXD1D,17,17,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,18,18,中国铁路南昌局集团有限公司 鹰潭机务段,,
|
|
||||||
HXD1D,19,19,中国铁路上海局集团有限公司 杭州机务段,,
|
|
||||||
HXD1D,20,20,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,21,21,中国铁路上海局集团有限公司 杭州机务段,,
|
|
||||||
HXD1D,22,24,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,25,25,中国铁路南昌局集团有限公司 鹰潭机务段,,
|
|
||||||
HXD1D,26,26,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,27,27,中国铁路上海局集团有限公司 杭州机务段,,
|
|
||||||
HXD1D,28,28,中国铁路南昌局集团有限公司 鹰潭机务段,,
|
|
||||||
HXD1D,29,34,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,35,35,中国铁路上海局集团有限公司 杭州机务段,,
|
|
||||||
HXD1D,36,38,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD1D,39,39,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD1D,40,50,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD1D,51,75,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
|
|
||||||
HXD1D,76,105,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD1D,106,137,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
HXD1D,138,168,中国铁路上海局集团有限公司 杭州机务段,,
|
|
||||||
HXD1D,169,175,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
HXD1D,176,185,中国铁路武汉局集团有限公司 武昌南机务段,,
|
|
||||||
HXD1D,186,187,中国铁路南昌局集团有限公司 鹰潭机务段,,
|
|
||||||
HXD1D,188,188,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,189,190,中国铁路南昌局集团有限公司 鹰潭机务段,,
|
|
||||||
HXD1D,191,232,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,233,233,中国铁路南昌局集团有限公司 鹰潭机务段,,
|
|
||||||
HXD1D,234,237,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,238,257,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
HXD1D,258,270,中国铁路武汉局集团有限公司 武昌南机务段,,
|
|
||||||
HXD1D,271,275,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
|
|
||||||
HXD1D,276,279,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
HXD1D,280,289,中国铁路上海局集团有限公司 徐州机务段,,
|
|
||||||
HXD1D,290,291,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,292,293,中国铁路南昌局集团有限公司 鹰潭机务段,,
|
|
||||||
HXD1D,294,295,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,296,300,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
HXD1D,301,310,中国铁路武汉局集团有限公司 武昌南机务段,,
|
|
||||||
HXD1D,311,320,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
|
|
||||||
HXD1D,321,340,中国铁路青藏集团有限公司 西宁机务段,,
|
|
||||||
HXD1D,341,362,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
|
|
||||||
HXD1D,363,382,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
HXD1D,383,392,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,393,405,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
HXD1D,406,415,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD1D,416,430,中国铁路广州局集团有限公司 长沙机务段,,
|
|
||||||
HXD1D,431,440,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,441,445,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,446,450,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
|
|
||||||
HXD1D,451,460,中国铁路武汉局集团有限公司 武昌南机务段,,
|
|
||||||
HXD1D,461,470,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
HXD1D,471,478,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD1D,479,483,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
HXD1D,484,488,中国铁路上海局集团有限公司 杭州机务段,,
|
|
||||||
HXD1D,489,490,中国铁路上海局集团有限公司 杭州机务段,,
|
|
||||||
HXD1D,491,510,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
HXD1D,511,512,中国铁路上海局集团有限公司 徐州机务段,,
|
|
||||||
HXD1D,513,515,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
HXD1D,516,520,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,521,534,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
HXD1D,522,522,广州铁路职业技术学院,,
|
|
||||||
HXD1D,535,544,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,545,551,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
HXD1D,552,554,中国铁路上海局集团有限公司 杭州机务段,,
|
|
||||||
HXD1D,555,559,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
HXD1D,560,564,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
|
|
||||||
HXD1D,565,570,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
HXD1D,571,585,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD1D,586,595,中国铁路上海局集团有限公司 杭州机务段,,
|
|
||||||
HXD1D,596,613,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD1D,614,623,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
HXD1D,624,633,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
HXD1D,634,636,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
|
|
||||||
HXD1D,637,644,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
HXD1D,645,660,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
HXD1D,661,668,中国铁路武汉局集团有限公司 武昌南机务段,,
|
|
||||||
HXD1D,669,673,中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段,,
|
|
||||||
HXD1D,674,678,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
HXD1D,679,682,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
HXD1D,683,683,中国铁路上海局集团有限公司 徐州机务段,,
|
|
||||||
HXD1D,684,684,中国铁路上海局集团有限公司 徐州机务段,,
|
|
||||||
HXD1D,685,689,中国铁路青藏集团有限公司 格尔木机务段,,
|
|
||||||
HXD1D,1898,1898,中国铁路上海局集团有限公司 上海机务段,周恩来号,
|
|
||||||
HXD1D-J,1,3,中国铁路青藏集团有限公司 拉萨动车运用所,,
|
|
||||||
HXD1D-J,1001,1009,中国铁路昆明局集团有限公司 昆明动车运用所,,
|
|
||||||
HXD1D-J,1010,1013,中国铁路青藏集团有限公司 格尔木机务段,,
|
|
||||||
HXD1D-J,1014,1019,中国铁路成都局集团有限公司 成都动车运用所,,
|
|
||||||
HXD1D-J,1020,1027,中国铁路昆明局集团有限公司 昆明动车运用所,,
|
|
||||||
HXD3C,1,9,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3C,10,10,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3C,11,15,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3C,16,20,中国铁路武汉局集团有限公司 江岸机务段(襄阳机务段支配),,
|
|
||||||
HXD3C,21,25,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD3C,26,30,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
HXD3C,31,35,中国铁路上海局集团有限公司 宁东机务段(上海机务段支配),,
|
|
||||||
HXD3C,36,41,中国铁路武汉局集团有限公司 江岸机务段(武南机务段支配),,
|
|
||||||
HXD3C,42,45,中国铁路武汉局集团有限公司 江岸机务段(襄阳机务段支配),,
|
|
||||||
HXD3C,46,55,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD3C,56,60,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
HXD3C,61,61,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3C,62,62,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3C,63,63,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3C,64,70,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3C,71,85,中国铁路武汉局集团有限公司 江岸机务段,,
|
|
||||||
HXD3C,86,95,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
HXD3C,96,100,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3C,101,110,中国铁路武汉局集团有限公司 江岸机务段,,
|
|
||||||
HXD3C,111,120,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3C,121,125,中国铁路上海局集团有限公司 宁东机务段(上海机务段支配),,
|
|
||||||
HXD3C,126,130,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD3C,131,135,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3C,136,140,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
HXD3C,141,165,中国铁路武汉局集团有限公司 江岸机务段,,
|
|
||||||
HXD3C,166,180,中国铁路成都局集团有限公司 重庆机务段,,
|
|
||||||
HXD3C,181,182,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3C,183,190,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3C,191,195,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3C,198,200,中国铁路上海局集团有限公司 宁东机务段,,
|
|
||||||
HXD3C,201,220,中国铁路武汉局集团有限公司 江岸机务段,,
|
|
||||||
HXD3C,221,225,中国铁路上海局集团有限公司 宁东机务段,,
|
|
||||||
HXD3C,226,229,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3C,238,238,中国铁路广州局集团有限公司 株洲机务段,,
|
|
||||||
HXD3C,271,300,中国铁路上海局集团有限公司 宁东机务段,,
|
|
||||||
HXD3C,446,446,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
HXD3C,805,809,中国铁路广州局集团有限公司 ,,
|
|
||||||
HXD3C,810,819,中国铁路南宁局集团有限公司,,
|
|
||||||
HXD3C,820,829,中国铁路武汉局集团有限公司,,
|
|
||||||
HXD3C,896,925,中国铁路沈阳局集团有限公司,,
|
|
||||||
HXD3C,926,930,中国铁路南宁局集团有限公司,,
|
|
||||||
HXD3C,931,945,中国铁路北京局集团有限公司,,
|
|
||||||
HXD3C,946,955,中国铁路济南局集团有限公司,,
|
|
||||||
HXD3C,956,965,中国铁路郑州局集团有限公司,,
|
|
||||||
HXD3C,966,974,中国铁路济南局集团有限公司,,
|
|
||||||
HXD3D,1,10,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3D,11,25,西安铁路局集团有限公司 西安机务段,,
|
|
||||||
HXD3D,26,34,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD3D,35,35,中国铁路兰州局集团有限公司 迎水桥机务段,雷锋号,
|
|
||||||
HXD3D,36,38,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD3D,39,39,中国铁路济南局集团有限公司 济南机务段,共青团号,
|
|
||||||
HXD3D,40,40,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD3D,41,50,西安铁路局集团有限公司 西安机务段,,
|
|
||||||
HXD3D,51,70,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
HXD3D,71,90,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD3D,91,115,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD3D,116,135,中国铁路昆明局集团有限公司 昆明机务段,,
|
|
||||||
HXD3D,136,145,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
HXD3D,146,150,呼和浩特铁路局集团有限公司 集宁机务段,,
|
|
||||||
HXD3D,151,155,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3D,156,160,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD3D,161,165,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
HXD3D,166,170,西安铁路局集团有限公司 西安机务段,,
|
|
||||||
HXD3D,171,180,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD3D,181,190,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3D,191,245,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3D,246,255,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
HXD3D,256,265,呼和浩特铁路局集团有限公司 集宁机务段,,
|
|
||||||
HXD3D,266,290,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD3D,291,300,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3D,301,310,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3D,310,315,中国铁路昆明局集团有限公司 昆明机务段,,
|
|
||||||
HXD3D,316,320,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD3D,321,322,呼和浩特铁路局集团有限公司 集宁机务段,,
|
|
||||||
HXD3D,323,325,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
HXD3D,326,333,西安铁路局集团有限公司 西安机务段,,
|
|
||||||
HXD3D,334,340,西安铁路局集团有限公司 安康机务段,,
|
|
||||||
HXD3D,341,345,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3D,346,346,中国铁路成都局集团有限公司 重庆机务段,,
|
|
||||||
HXD3D,351,351,中国铁路成都局集团有限公司 重庆机务段,,
|
|
||||||
HXD3D,356,365,西安铁路局集团有限公司 安康机务段,,
|
|
||||||
HXD3D,366,369,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
HXD3D,370,382,呼和浩特铁路局集团有限公司 集宁机务段,,
|
|
||||||
HXD3D,383,392,中国铁路昆明局集团有限公司 昆明机务段,,
|
|
||||||
HXD3D,393,397,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD3D,398,402,西安铁路局集团有限公司 西安机务段,,
|
|
||||||
HXD3D,403,417,西安铁路局集团有限公司 西安机务段,,
|
|
||||||
HXD3D,418,419,中国铁路哈尔滨局集团有限公司 牡丹江机务段,,
|
|
||||||
HXD3D,420,424,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
HXD3D,425,429,呼和浩特铁路局集团有限公司 集宁机务段,,
|
|
||||||
HXD3D,430,433,中国铁路哈尔滨局集团有限公司 牡丹江机务段,,
|
|
||||||
HXD3D,434,443,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD3D,444,449,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3D,450,464,西安铁路局集团有限公司 西安机务段,,
|
|
||||||
HXD3D,465,468,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3D,469,473,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3D,474,479,中国铁路昆明局集团有限公司 昆明机务段,,
|
|
||||||
HXD3D,480,484,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD3D,485,489,西安铁路局集团有限公司 西安机务段,,
|
|
||||||
HXD3D,490,499,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
HXD3D,500,503,中国铁路哈尔滨局集团有限公司 牡丹江机务段,,
|
|
||||||
HXD3D,504,514,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3D,515,515,中国铁路成都局集团有限公司 重庆机务段,,
|
|
||||||
HXD3D,516,518,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3D,519,528,西安铁路局集团有限公司 西安机务段,,
|
|
||||||
HXD3D,529,538,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
HXD3D,539,541,呼和浩特铁路局集团有限公司 集宁机务段,,
|
|
||||||
HXD3D,542,553,西安铁路局集团有限公司 西安机务段,,
|
|
||||||
HXD3D,554,563,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3D,564,568,中国铁路昆明局集团有限公司 昆明机务段,,
|
|
||||||
HXD3D,569,573,中国铁路成都局集团有限公司 重庆机务段,,
|
|
||||||
HXD3D,574,583,中国铁路济南局集团有限公司 济南机务段,,
|
|
||||||
HXD3D,584,584,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3D,585,609,中国铁路哈尔滨局集团有限公司 三棵树机务段,,
|
|
||||||
HXD3D,610,611,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
HXD3D,612,621,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD3D,622,626,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
HXD3D,627,629,中国铁路哈尔滨局集团有限公司 三棵树机务段,,
|
|
||||||
HXD3D,630,630,中国铁路哈尔滨局集团有限公司 哈尔滨机务段,,
|
|
||||||
HXD3D,631,631,西安铁路局集团有限公司 西安机务段,第五代“钢人铁马号”,
|
|
||||||
HXD3D,632,653,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3D,654,673,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3D,674,681,中国铁路沈阳局集团有限公司 沈阳机务段,,
|
|
||||||
HXD3D,682,688,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
HXD3D,1886,1886,中国铁路哈尔滨局集团有限公司 哈尔滨机务段,第五代“朱德号”,
|
|
||||||
HXD3D,1893,1893,中国铁路北京局集团有限公司 丰台机务段,第六代“毛泽东号”,
|
|
||||||
HXD3D,1921,1921,中国铁路沈阳局集团有限公司 沈阳机务段,共产党员号,
|
|
||||||
HXD3D,7001,7002,广西沿海铁路股份有限公司 南宁南机务运用段,,
|
|
||||||
HXD3D,7003,7003,吉林铁道职业技术学院,,
|
|
||||||
HXD3D,8001,8025,中国铁路沈阳局集团有限公司 沈阳机务段,,大同
|
|
||||||
HXD3D,8026,8028,中国铁路太原局集团有限公司 太原南机务段,,大同
|
|
||||||
东方红2,1,50,,,资阳
|
|
||||||
东风,1201,1830,,,大连、成都
|
|
||||||
东风,2001,2094,,,戚墅堰
|
|
||||||
东风11,1,459,,,戚墅堰
|
|
||||||
东风12,8001,8001,吉林铁道职业技术学院,,
|
|
||||||
东风2,3201,3348,,,戚墅堰
|
|
||||||
东风21,1,5,中国铁路昆明局集团有限公司 昆明机务段,,
|
|
||||||
东风21,6,6,中国铁路昆明局集团有限公司 昆明机务段,状元号,
|
|
||||||
东风21,7,7,中国铁路昆明局集团有限公司 昆明机务段,亲年号,
|
|
||||||
东风21,8,8,中国铁路昆明局集团有限公司 昆明机务段,建水古城,
|
|
||||||
东风21,9,100,中国铁路昆明局集团有限公司 昆明机务段,,
|
|
||||||
东风21,101,101,中国铁路昆明局集团有限公司 昆明机务段,异龙号,
|
|
||||||
东风21,102,102,中国铁路昆明局集团有限公司 昆明机务段,,
|
|
||||||
东风21,1001,1002,云南钢铁厂,,
|
|
||||||
东风2Z,3251,3251,*齐齐哈尔铁路局 加格达奇机务段,,
|
|
||||||
东风3,3243,3243,中车共享城机车公园,,
|
|
||||||
东风4,3247,3247,中车成都轨道交通产业园,,
|
|
||||||
东风4B,1001,1999,,,大连
|
|
||||||
东风4B,1963,1963,*北京铁路局 丰台机务段,,
|
|
||||||
东风4B,2101,2685,,,大连
|
|
||||||
东风4B,2104,2104,*上海铁路局 蚌埠机务段,,
|
|
||||||
东风4B,2376,2376,*南昌铁路局 鹰潭机务段,,
|
|
||||||
东风4B,3101,3999,,,资阳
|
|
||||||
东风4B,3214,3214,*浙江金温铁道开发有限公司 温州机务段,,
|
|
||||||
东风4B,3249,3249,*西安铁路局 西安机务段,,
|
|
||||||
东风4B,3390,3390,*成都铁路局 重庆机务段,,
|
|
||||||
东风4B,3593,3593,*中国铁路广州局集团有限公司 株洲机务段,,
|
|
||||||
东风4B,6001,6587,,,大同
|
|
||||||
东风4B,6530,6530,*南宁铁路局 南宁机务段,,
|
|
||||||
东风4B,7001,7363,,,大连
|
|
||||||
东风4B,7364,7365,,,四方
|
|
||||||
东风4B,7366,7796,,,大连
|
|
||||||
东风4B,7701,7732,,,戚墅堰改
|
|
||||||
东风4B,9001,9702,,,资阳
|
|
||||||
东风4B,9167,9167,*南昌铁路局 向塘机务段,,
|
|
||||||
东风4B,9531,9531,*新长铁路公司,,
|
|
||||||
东风4C,1,10,,,大同
|
|
||||||
东风4C,11,11,中国铁路北京局集团有限公司 丰台段,青年文明号,
|
|
||||||
东风4C,12,40,,,大同
|
|
||||||
东风4C,2001,2006,,,四方
|
|
||||||
东风4C,4001,4465,,,大连
|
|
||||||
东风4C,4466,4466,四方机车车辆厂,,四方
|
|
||||||
东风4C,5001,5273,,,资阳
|
|
||||||
东风4C,5274,5275,三茂铁路公司 三水机务段,东风4CK,
|
|
||||||
东风4C,5276,5335,,,资阳
|
|
||||||
东风4D,7001,7021,中国铁路南宁局集团有限公司,,
|
|
||||||
东风5,1,1,中国铁路北京局集团有限公司 北京车辆段,,
|
|
||||||
东风5,1974,1975,中国铁路兰州局集团有限公司 兰州西机务段,,唐山
|
|
||||||
东风5,1976,2082,,,唐山
|
|
||||||
东风5,2083,2083,中国石油兰州石化公司,,唐山
|
|
||||||
东风5,3279,3279,云南铁路博物馆,,
|
|
||||||
东风6,1,2,*沈阳铁路局 大连机务段,,
|
|
||||||
东风6,3,3,沈阳铁路陈列馆,,
|
|
||||||
东风6,4,4,*沈阳铁路局 大连机务段,,
|
|
||||||
东风7,174,174,中国铁路太原局集团有限公司 太原机务段北场,,
|
|
||||||
东风7B,3006,3006,中国铁道博物馆,,
|
|
||||||
东风7B,3015,3015,王坪村铁路公园,,
|
|
||||||
东风7B,6001,6072,*北京铁路局 邯郸机务段;郑州铁路局 新乡机务段,调车,
|
|
||||||
东风7D,1,1,中国铁道博物馆,,
|
|
||||||
东风7D,3001,3001,中国铁道博物馆,,
|
|
||||||
东风7E,1,1,中国铁路郑州局集团有限公司 新乡机务段,,
|
|
||||||
东风7E,2,2,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
东风7G,9001,9004,呼和浩特铁路局 集宁机务段 赛汗塔拉分段,,
|
|
||||||
东风8,1,1,中国铁道博物馆,,
|
|
||||||
东风9,1,2,中国铁路广州局集团有限公司广州机务段,,
|
|
||||||
韶山1,8,8,中国铁道博物馆,,
|
|
||||||
韶山1,156,156,郑州世纪欢乐园,,
|
|
||||||
韶山1,160,160,北京铁路电气化学校,,
|
|
||||||
韶山1,227,227,中国铁路兰州局集团有限公司 兰州西机务段,,
|
|
||||||
韶山1,254,254,中国铁路北京局集团有限公司 丰台机务段 储备厂,,
|
|
||||||
韶山1,307,307,中国铁路太原局集团有限公司 榆次机务折返段,,
|
|
||||||
韶山1,309,309,中国铁路太原局集团有限公司 太原机务段北场,,
|
|
||||||
韶山1,321,321,武汉铁路职业技术学院,,
|
|
||||||
韶山1,681,681,中国铁道博物馆,,
|
|
||||||
韶山1,695,695,沈阳铁路陈列馆,,
|
|
||||||
韶山1,762,762,中国铁路广州局集团有限公司 娄底运用车间储备厂,,
|
|
||||||
韶山1,818,818,西南交通大学 机车博物园,,
|
|
||||||
韶山1,821,821,韶关机务实训基地,,
|
|
||||||
韶山1,826,826,韶关机务实训基地,,
|
|
||||||
韶山3,454,454,中国铁路成都局集团有限公司 贵阳机务段,先锋号,
|
|
||||||
韶山3,524,524,中国铁路武汉局集团有限公司 江岸机务段,青年号,
|
|
||||||
韶山3,4160,4160,广西沿海铁路公司 南宁南机务运用段,共青团号,
|
|
||||||
韶山3,4178,4178,广西沿海铁路公司 南宁南机务运用段,共青团号,
|
|
||||||
韶山3,4235,4235,中国铁路成都局集团有限公司 重庆机务段,青年文明号,
|
|
||||||
韶山3,4258,4258,中国铁路成都局集团有限公司 重庆机务段,党员先锋号,
|
|
||||||
韶山3,5080,5080,广州铁路博物馆,,
|
|
||||||
韶山3,6005,6005,湖南交通工程学院,,
|
|
||||||
韶山3,8050,8050,武汉四美塘铁路遗址公园,,
|
|
||||||
韶山3B,16,16,西安铁路局 安康机务段,青年文明号,
|
|
||||||
韶山3B,5001,5001,中国铁路成都局集团有限公司 贵阳机务段,*先锋力神,
|
|
||||||
韶山3B,5035,5035,中国铁路兰州局集团有限公司 迎水桥机务段,雷锋号 (曾),
|
|
||||||
韶山3B,5038,5038,中国铁路兰州局集团有限公司 迎水桥机务段,青年文明号,
|
|
||||||
韶山3B,5151,5151,中国铁路成都局集团有限公司 西昌机务段,扶贫先锋号,
|
|
||||||
韶山3B,5162,5162,中国铁路昆明局集团有限公司 昆明机务段,五四青年号,
|
|
||||||
韶山3B,5235,5235,中国铁路成都局集团有限公司 西昌机务段,*共青团号,
|
|
||||||
韶山3C,1,1,中国铁路成都局集团有限公司 贵阳机务段,,
|
|
||||||
韶山4,6,6,中国铁道博物馆,,
|
|
||||||
韶山4,10,10,中国铁路成都局集团有限公司 西昌机务段,,
|
|
||||||
韶山4,50,50,中国铁路郑州局集团有限公司 新乡机务段,先锋号,
|
|
||||||
韶山4,63,63,中国铁路太原局集团有限公司 太原机务段,,
|
|
||||||
韶山4,204,204,中国铁路郑州局集团有限公司 新乡机务段,先锋号,
|
|
||||||
韶山4,448,448,中国铁路沈阳局集团有限公司 苏家屯机务段,先锋号,
|
|
||||||
韶山4,574,574,中铁三局集团,先锋号,
|
|
||||||
韶山4,743,743,中国铁路哈尔滨局集团有限公司 哈尔滨机务段,青年文明号,
|
|
||||||
韶山4,855,855,西安铁路局 新丰镇机务段,,
|
|
||||||
韶山4,911,911,中铁三局集团,青年文明号,
|
|
||||||
韶山4,2006,2006,吉林铁道职业技术学院,,
|
|
||||||
韶山4B,19,19,神朔铁路公司 神木北机务段,青年号,
|
|
||||||
韶山4B,89,89,神朔铁路公司 神木北机务段,青年文明号,
|
|
||||||
韶山4B,90,90,神朔铁路公司 神木北机务段,青年文明号,
|
|
||||||
韶山4B,257,257,包神铁路公司 东胜机务段,党员先锋号,
|
|
||||||
韶山4G,159,1177,,,株洲
|
|
||||||
韶山4G,168,168,中国铁道博物馆,,
|
|
||||||
韶山4G,171,171,中国铁路哈尔滨局集团有限公司 牡丹江机务段,,
|
|
||||||
韶山4G,179,179,中国铁路太原局集团有限公司 湖东机务段,,
|
|
||||||
韶山4G,466,466,石家庄铁道大学,,
|
|
||||||
韶山4G,1089,1089,*呼和浩特铁路局 包头西机务段,,
|
|
||||||
韶山4G,1886,1886,中国铁路哈尔滨局集团有限公司 哈尔滨机务段,*朱德号,株洲
|
|
||||||
韶山4G,3001,3002,,,资阳
|
|
||||||
韶山4G,6001,6001,中国铁道博物馆,,
|
|
||||||
韶山4G,6001,6001,中国铁道博物馆,,大同
|
|
||||||
韶山4G,7001,7110,,,大连
|
|
||||||
韶山4G,7121,7243,,,大连
|
|
||||||
韶山5,1,1,中国铁道博物馆,,
|
|
||||||
韶山5,2,2,郑州世纪欢乐园 ,,
|
|
||||||
韶山6,1,1,郑州铁路司机学校,,
|
|
||||||
韶山6,2,2,中国铁道博物馆,,
|
|
||||||
韶山6B,1011,1011,西安铁路局 西安机务段,*青年文明号,
|
|
||||||
韶山6B,1026,1026,韶关机务实训基地,,
|
|
||||||
韶山6B,1088,1088,中国铁路武汉局集团有限公司 襄阳机务段,*民兵号,
|
|
||||||
韶山6B,1111,1111,中国铁路武汉局集团有限公司 襄阳机务段,*先锋号,
|
|
||||||
韶山6B,6001,6001,韶关机务实训基地,,
|
|
||||||
韶山6B,6002,6002,广州铁路博物馆,,
|
|
||||||
韶山7,1,79,中国铁路南宁局集团有限公司 柳州机务段,,
|
|
||||||
韶山7,76,76,中国铁路南宁局集团有限公司 南宁机务段,*五四红旗号,
|
|
||||||
韶山7,80,84,中国铁路南宁局集团有限公司 柳州机务段,,
|
|
||||||
韶山7,85,111,中国铁路南宁局集团有限公司 柳州机务段,,
|
|
||||||
韶山7,8112,8113,山西孝柳铁路有限责任公司,,
|
|
||||||
韶山7B,1,1,*南宁铁路局集团有限公司 南宁机务段,,
|
|
||||||
韶山7B,2,2,中国铁路南宁局集团有限公司 柳州机务段,,
|
|
||||||
韶山7D,1,58,西安铁路局集团有限公司 西安机务段,,
|
|
||||||
韶山7D,631,631,西安铁路局集团有限公司 西安机务段,*钢人铁马号,
|
|
||||||
韶山7E,1,140,,,大同
|
|
||||||
韶山7E,6001,6002,中国铁路昆明局集团有限公司,,大同
|
|
||||||
韶山7E,7001,7004,,,大连
|
|
||||||
韶山8,1,1,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,2,2,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,3,4,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
韶山8,5,5,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,9,9,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,11,11,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,12,12,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,15,16,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,17,17,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
韶山8,20,20,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,24,25,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,27,27,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,29,32,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,33,35,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
韶山8,36,36,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,38,38,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
韶山8,39,39,中国铁路上海局集团有限公司 上海机务段,国祥号,
|
|
||||||
韶山8,40,40,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
韶山8,41,41,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
韶山8,43,43,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,44,44,中国铁路北京局集团有限公司 邯郸机务段,,
|
|
||||||
韶山8,45,45,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,48,48,中国铁路北京局集团有限公司 邯郸机务段,,
|
|
||||||
韶山8,49,49,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
韶山8,50,50,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
韶山8,51,51,中国铁路北京局集团有限公司 邯郸机务段,,
|
|
||||||
韶山8,52,52,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
韶山8,55,55,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
韶山8,56,57,中国铁路北京局集团有限公司 邯郸机务段,,
|
|
||||||
韶山8,64,64,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,72,72,中国铁路北京局集团有限公司 邯郸机务段,,
|
|
||||||
韶山8,73,73,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
韶山8,74,74,中国铁路北京局集团有限公司 邯郸机务段,,
|
|
||||||
韶山8,81,81,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
韶山8,83,84,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,85,85,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
韶山8,88,103,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,104,104,中国铁路北京局集团有限公司 邯郸机务段,,
|
|
||||||
韶山8,109,111,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
韶山8,114,116,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
韶山8,118,119,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
韶山8,121,126,中国铁路北京局集团有限公司 北京机务段,,
|
|
||||||
韶山8,127,128,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,130,130,中国铁路南昌局集团有限公司 南昌机务段,,
|
|
||||||
韶山8,131,131,中国铁路广州局集团有限公司 长沙机务段,,
|
|
||||||
韶山8,132,132,中国铁路广州局集团有限公司 长沙机务段,,
|
|
||||||
韶山8,133,133,中国铁路广州局集团有限公司 长沙机务段,,
|
|
||||||
韶山8,134,134,中国铁路广州局集团有限公司 长沙机务段,,
|
|
||||||
韶山8,136,136,中国铁路广州局集团有限公司 长沙机务段,,
|
|
||||||
韶山8,141,141,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,144,144,中国铁路广州局集团有限公司 长沙机务段,,
|
|
||||||
韶山8,148,148,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,156,156,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,163,163,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,166,166,中国铁路广州局集团有限公司 广州机务段,新世纪金龙号,
|
|
||||||
韶山8,171,171,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
韶山8,172,172,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,173,173,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,181,181,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,186,186,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,191,191,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,192,192,中国铁路广州局集团有限公司 广州机务段,,
|
|
||||||
韶山8,197,197,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山8,200,204,中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
韶山8,205,205,中国铁路广州局集团有限公司 长沙机务段,,
|
|
||||||
韶山8,214,214,中国铁路郑州局集团有限公司 郑州机务段,,
|
|
||||||
韶山9,1,3,中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
韶山9,5,29,中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
韶山9,30,30,中国铁路沈阳局集团有限公司 通辽机务段,,
|
|
||||||
韶山9,31,37,中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
韶山9,38,38,中国铁路沈阳局集团有限公司 通辽机务段,,
|
|
||||||
韶山9,39,43,中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段,,
|
|
||||||
|
Reference in New Issue
Block a user