18 Commits

Author SHA1 Message Date
undef-i
44bc27a366 chore: update loco_type_info.csv 2025-09-03 19:04:10 +08:00
undef-i
0d6c11e2bd chore 2025-08-29 13:27:25 +08:00
undef-i
92445e681f chore 2025-08-28 21:15:13 +08:00
Nedifinita
78cc909ec8 feat: add database import and export functions 2025-08-28 19:48:12 +08:00
Nedifinita
077e0e4266 refactor: clean up dead code 2025-08-28 14:14:45 +08:00
Nedifinita
0bf7033c6c feat: update locomotive type support and optimized position display format 2025-08-19 19:11:52 +08:00
Nedifinita
0f98b6bcf7 refactor: optimize train record merging logic 2025-08-19 18:06:02 +08:00
Nedifinita
8894a73999 fix: optimize the display logic of history item spacing 2025-08-19 17:43:21 +08:00
Nedifinita
cd4b58e16b fix: correct the default value handling issue when the train display is empty 2025-08-19 17:03:09 +08:00
Nedifinita
39effddfc1 feat: add LocoTypeUtil 2025-08-19 16:35:47 +08:00
undef-i
c4b06f3b3c fix: typo 2025-08-17 14:24:54 +08:00
Nedifinita
eb33fa7feb chore: update project name and .gitignore file 2025-08-08 19:20:26 +08:00
undef-i
65bf7b52c6 fix: correct the error in train_number_info.csv 2025-08-05 20:43:16 +08:00
undef-i
4278de2a8d fix: correct incorrect rendering and status of map marker points 2025-08-03 18:40:14 +08:00
Nedifinita
59e9987d7f feat: add train type recognition and restructure settings storage 2025-08-01 20:07:35 +08:00
Nedifinita
4e97dcafd7 fix: hide duplicate info when train and loco both match 2025-08-01 17:51:24 +08:00
Nedifinita
4cad3679a9 fix: improve TRAIN_OR_LOCO merge display and settings scroll 2025-08-01 17:44:26 +08:00
Nedifinita
e6e7831b96 refactor: migrate data storage from SharedPreferences to Room database 2025-08-01 17:36:04 +08:00
59 changed files with 1982 additions and 1644 deletions

149
.gitignore vendored
View File

@@ -13,10 +13,155 @@ captures
.externalNativeBuild
.cxx
local.properties
local.properties
*.ps1
.*.bat
*.jks
*.keystore
*.base64
docs
docs
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
View File

@@ -1 +1 @@
LBJ Receiver
LBJ_Console

View File

@@ -2,6 +2,12 @@
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="Unnamed">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="lbjconsole_android">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>

View File

@@ -1,8 +1,20 @@
# LBJ Console
LBJ Console is an Android app designed to receive and display LBJ messages via BLE from the [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 信息。
- 基于内置数据文件显示机车配属,机车类型和车次类型。
主分支目前只适配了 Android 。如需在其它平台上面使用,请参考 [flutter](https://github.com/undef-i/LBJ_Console/tree/flutter) 分支自行编译。
## 数据文件
LBJ Console 依赖以下数据文件,位于 `app/src/main/assets/` 目录,用于支持机车配属和车次信息的展示:
- `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注`
- `loco_type_info.csv`:包含机车类型编码信息,格式为 `机车类型编码,机车类型`
- `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型`
# License
# 许可证
This project is licensed under the GNU General Public License v3.0 (GPLv3). This license ensures that the software remains free and open source, requiring that any modifications or derivative works must also be released under the same license terms.
该项目采用 GNU 通用公共许可证 v3.0GPLv3)授权。该许可证确保软件保持免费和开源,要求任何修改或衍生作品也必须在相同许可证条款下发布。

View File

@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
}
android {
@@ -12,8 +13,8 @@ android {
applicationId = "org.noxylva.lbjconsole"
minSdk = 29
targetSdk = 35
versionCode = 8
versionName = "0.0.8"
versionCode = 15
versionName = "0.1.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -59,6 +60,7 @@ android {
}
lint {
disable += "NullSafeMutableLiveData"
warning += "MissingPermission"
}
}
@@ -85,4 +87,10 @@ dependencies {
implementation("org.osmdroid:osmdroid-android:6.1.16")
implementation("org.osmdroid:osmdroid-mapsforge:6.1.16")
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.startup.runtime)
implementation("com.google.code.gson:gson:2.10.1")
}

View File

@@ -14,6 +14,10 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<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"/>
@@ -41,11 +45,10 @@
</activity>
<activity
android:name=".SettingsActivity"
android:name=".FilePickerActivity"
android:exported="false"
android:label="Settings"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.LBJConsole" />
android:theme="@style/Theme.LBJConsole"
android:label="数据管理" />
<service
android:name=".BackgroundService"

View 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
1 001 解放
2 003 前进
3 005 建设
4 006 KD7
5 055 蓝箭控车
6 081 东风21
7 101 东风
8 102 东风2
9 103 东风3
10 104 东风4
11 105 东风4客
12 106 东风4C
13 107 东风5
14 108 东风5宽
15 109 东风6
16 110 东风7
17 111 东风8
18 112 东风9
19 113 东风10
20 114 东方红1
21 115 东方红2
22 116 东方红3
23 117 东方红5
24 118 北京
25 119 北京宽
26 120 ND2
27 121 ND3
28 122 ND4
29 123 ND5
30 124 NY5
31 125 NY6
32 126 NY7
33 127 轻油
34 128 东方红21
35 129 东风7B
36 130 东风5S
37 131 东风7C
38 132 东风7S
39 133 工矿1
40 134 工矿1F
41 135 东风4E
42 136 东风7D
43 137 工矿1A
44 138 东风11
45 139 天安
46 140 东风10F
47 141 东风4D
48 142 东风8B
49 143 东风12
50 144 东风7E
51 145 NYJ1
52 146 NZJ1
53 147 NZJ2
54 148 东风4DJ
55 149 新曙光
56 150 神州
57 151 NJ2
58 152 东风7G
59 153 NDJ3
60 157 FXN3D
61 158 东风11G
62 160 HXN3
63 161 HXN5
64 162 HXN3B
65 163 HXN5B
66 167 FXN3B
67 169 FXN3C
68 170 FXN5C
69 171 FXN3-J
70 201 8G
71 202 8K
72 203 6G
73 204 6K
74 205 韶山1
75 206 韶山3
76 207 韶山4
77 208 韶山5
78 209 韶山6
79 210 韶山3B
80 211 韶山7
81 212 韶山8
82 213 韶山7B
83 214 韶山7C
84 215 韶山6B
85 216 韶山9
86 217 韶山7D
87 218 DJ熊猫
88 219 DJ1
89 220 DJ2
90 221 DJF
91 222 蓝箭动车
92 223 先锋号
93 224 韶山7E
94 225 韶山4G
95 226 韶山3C
96 228 天梭
97 229 DJ4和谐
98 230 KTT
99 231 HXD1
100 232 HXD2
101 233 HXD3
102 234 HXD1B
103 235 HXD2B
104 236 HXD3B
105 237 HXD1C
106 238 HXD2C
107 239 HXD3C
108 240 HXD1D
109 241 HXD2D
110 242 HXD3D
111 243 FXD1B
112 244 FXD2B
113 245 FXD1
114 246 FXD3
115 247 FXD1-J
116 248 FXD3-J
117 249 KZ25TA
118 251 KZ25TB
119 252 HXD1D-J
120 254 FXD1H
121 300 雪域神州
122 301 CRH1
123 302 CRH2
124 303 CRH3
125 305 CRH5
126 306 CRH380A
127 307 CRH380B
128 308 CRH380C
129 309 CRH380D
130 310 CRH6A
131 311 CR400AF
132 312 CR400BF
133 313 CR300AF
134 314 CR300BF
135 315 CRH2E
136 316 CRH6F
137 330 CJ1
138 331 CJ2
139 332 CJ3
140 333 CJ4
141 334 CJ5
142 335 CJ6
143 400 GCD-1000J

View File

@@ -0,0 +1,82 @@
"^[Gg](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定高速动车组"
"^[Gg](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客高速动车组"
"^[Gg](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定高速动车组"
"^[Gg](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","管内临客高速动车组"
"^[Cc]([1-8]\d{3}|9000)$","图定城际动车组"
"^[Cc](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","临客城际动车组"
"^[Cc][1-9]\d{2}$","动力集中城际动车组"
"^[IDid](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定动车组"
"^[IDid](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客动车组"
"^[IDid](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定动车组"
"^[IDid](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","管内临客动车组"
"^[IDid](8([0-8]\d|9[0-8])|7(0[1-9]|[1-9]\d))$","动力集中动车组"
"^[IDid](300|[12]\d{2}|[1-9]\d?)$","跨局动力集中动车组"
"^[PZpz](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定直达特快旅客列车"
"^[PZpz](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客直达特快旅客列车"
"^[PZpz](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定直达特快旅客列车"
"^[PZpz](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","管内临客直达特快旅客列车"
"^[QTqt](3000|[12]\d{3}|[1-9]\d{0,2})$","直通图定特快旅客列车"
"^[QTqt](300[1-9]|30[1-9]\d|3[1-8]\d{2}|39[0-8]\d|399[0-8])$","直通临客特快旅客列车"
"^[QTqt](4(00[1-9]|0[1-9]\d|[1-8]\d{2}|9[0-8]\d|99[0-8]))$","管内临客特快旅客列车"
"^[QTqt]([5-8]\d{3}|9([0-8]\d{2}|9[0-8]\d|99[0-8])|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定特快旅客列车"
"^[WKwk](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定快速旅客列车"
"^[WKwk](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客快速旅客列车"
"^[WKwk](6([0-8]\d{2}|9[0-8]\d|99[0-8])|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内临客快速旅客列车"
"^[WKwk](8\d{3}|9([0-8]\d{2}|9[0-8]\d|99[0-8])|7(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})$","跨两局图定普通旅客快车"
"^3(00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨局临时普通旅客快车"
"^[Uu4](00[1-9]|0[1-9]\d|[1-9]\d{2})$","管内图定普通旅客快车"
"^[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(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}))$","通勤列车"
"^[Yy](500|[1-4]\d{2}|[1-9]\d?)$","跨局旅游列车"
"^[Yy](50[1-9]|5[1-9]\d|[6-9]\d{2})$","管内旅游列车"
"^[Ss][1-9]\d{0,3}$","市郊旅客列车"
"^[Ll](6([0-8]\d{2}|9[0-8]\d|99[0-8])|[1-5]\d{3}|[1-9]\d{0,2})$","直通临时旅客列车"
"^[Ll]([7-9]\d{3})$","管内临时旅客列车"
"^[Xx](19[0-8]|1[0-8]\d|[1-9]\d?)$","特快货物班列"
"^[Xx](39[0-8]|3[0-8]\d|2[1-9]\d|20[1-9])$","快速货物班列"
"^[Xx]2(40[1-9]|4[1-9]\d|[5-9]\d{2})$","直通货物快运列车"
"^[Xx]([4-9]\d{2}|4[1-9]\d|40[1-9])$","管内货物快运列车"
"^[Xx]8\d{3}$","中欧中亚集装箱班列"
"^[Xx]9([0-4]\d{2}|500)$","中亚集装箱班列"
"^[Xx]9(50[1-9]|5[1-9]\d|[6-9]\d{2})$","水铁联运班列"
"^[Xx][1-4]\d{4}$","加挂零散快运车辆货物列车"
"^1(000[1-9]|00[1-9]\d|0[1-9]\d{2}|[1-9]\d{3})$","技术直达列车"
"^2\d{4}$","直通货物列车"
"^3\d{4}$","区段摘挂列车"
"^4([0-3]\d{3}|4([0-8]\d{2}|9[0-8]\d|99[0-8]))$","摘挂列车"
"^4(500[1-9]|50[1-9]\d|5[1-9]\d{2}|[6-9]\d{3})$","小运转列车"
"^6\d{4}$","自备列车"
"^70\d{3}$","超限货物列车"
"^7([1-6]\d{3}|7([0-8]\d{2}|9[0-8]\d|99[0-8]))$","重载货物列车"
"^78\d{3}$","保温列车"
"^8(0\d{3}|1([0-8]\d{2}|9[0-8]\d|99[0-8]))$","普快货物班列"
"^8(200[1-9]|20[1-9]\d|2[1-9]\d{2}|[34]\d{3})$","煤炭直达列车"
"^85\d{3}$","石油直达列车"
"^86\d{3}$","始发直达列车"
"^87\d{3}$","空车直达列车"
"^(90\d{3}|91([0-8]\d{2}|9[0-8]\d|99[0-8]))$","军用列车"
"^50\d{3}$","客车单机"
"^51\d{3}$","货车单机"
"^52\d{3}$","小运转单机"
"^5(3\d{3}|4([0-8]\d{2}|9[0-8]\d|99[0-8]))$","补机列车"
"^55(300|[0-2]\d{2})$","普通客货试运转列车"
"^55(500|30[1-9]|3[1-9]\d|4\d{2})$","高速动车组试运转列车"
"^55(50[1-9]|5[1-9]\d|[6-9]\d{2})$","普通动车组试运转列车"
"^56\d{3}$","轻油动车与轨道车"
"^57\d{3}$","路用列车"
"^58(10[1-9]|1[1-9]\d|[2-8]\d{2}|9([0-8]\d|9[0-8]))$","救援列车"
"^DJ(400|[1-3]\d{2}|[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(40[1-9]|4[1-9]\d|[5-9]\d{2})$","动车组检测列车250管内"
"^DJ[56]\d{3}$","直通动车组确认列车"
"^DJ[78]\d{3}$","管内动车组确认列车"
"^[Ff][GDCZTKgdcztk]?\d{1,4}$","因故折返旅客列车"
"^0[GDCZTKgdcztk]\d{1,4}$","回送图定客车底"
"^00(100|[1-9]\d?)$","有火回送动车组车底"
"^00(10[1-9]|1[1-9]\d|2([0-8]\d|9[0-8]))$","无火回送动车组车底"
"^00(30[1-9]|3[1-9]\d|4([0-8]\d|9[0-8]))$","无火回送普速客车底"
1 ^[Gg](4000|[1-3]\d{3}|[1-9]\d{0,2})$ 直通图定高速动车组
2 ^[Gg](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$ 直通临客高速动车组
3 ^[Gg](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$ 管内图定高速动车组
4 ^[Gg](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$ 管内临客高速动车组
5 ^[Cc]([1-8]\d{3}|9000)$ 图定城际动车组
6 ^[Cc](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$ 临客城际动车组
7 ^[Cc][1-9]\d{2}$ 动力集中城际动车组
8 ^[IDid](4000|[1-3]\d{3}|[1-9]\d{0,2})$ 直通图定动车组
9 ^[IDid](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$ 直通临客动车组
10 ^[IDid](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$ 管内图定动车组
11 ^[IDid](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$ 管内临客动车组
12 ^[IDid](8([0-8]\d|9[0-8])|7(0[1-9]|[1-9]\d))$ 动力集中动车组
13 ^[IDid](300|[12]\d{2}|[1-9]\d?)$ 跨局动力集中动车组
14 ^[PZpz](4000|[1-3]\d{3}|[1-9]\d{0,2})$ 直通图定直达特快旅客列车
15 ^[PZpz](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$ 直通临客直达特快旅客列车
16 ^[PZpz](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$ 管内图定直达特快旅客列车
17 ^[PZpz](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$ 管内临客直达特快旅客列车
18 ^[QTqt](3000|[12]\d{3}|[1-9]\d{0,2})$ 直通图定特快旅客列车
19 ^[QTqt](300[1-9]|30[1-9]\d|3[1-8]\d{2}|39[0-8]\d|399[0-8])$ 直通临客特快旅客列车
20 ^[QTqt](4(00[1-9]|0[1-9]\d|[1-8]\d{2}|9[0-8]\d|99[0-8]))$ 管内临客特快旅客列车
21 ^[QTqt]([5-8]\d{3}|9([0-8]\d{2}|9[0-8]\d|99[0-8])|500[1-9]|50[1-9]\d|5[1-9]\d{2})$ 管内图定特快旅客列车
22 ^[WKwk](4000|[1-3]\d{3}|[1-9]\d{0,2})$ 直通图定快速旅客列车
23 ^[WKwk](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$ 直通临客快速旅客列车
24 ^[WKwk](6([0-8]\d{2}|9[0-8]\d|99[0-8])|500[1-9]|50[1-9]\d|5[1-9]\d{2})$ 管内临客快速旅客列车
25 ^[WKwk](8\d{3}|9([0-8]\d{2}|9[0-8]\d|99[0-8])|7(00[1-9]|0[1-9]\d|[1-9]\d{2}))$ 管内图定快速旅客列车
26 ^[Vv1](00[1-9]|0[1-9]\d|[1-9]\d{2})$ 跨三局及以上图定普通旅客快车
27 ^[Bb2](00[1-9]|0[1-9]\d|[1-9]\d{2})$ 跨两局图定普通旅客快车
28 ^3(00[1-9]|0[1-9]\d|[1-9]\d{2})$ 跨局临时普通旅客快车
29 ^[Uu4](00[1-9]|0[1-9]\d|[1-9]\d{2})$ 管内图定普通旅客快车
30 ^[Xx5](000|1[9][9]|200|3[9][9]|400)$ 管内图定普通旅客快车
31 ^6(19[0-8]|1[0-8]\d|0[1-9]\d|00[1-9])$ 直通普通旅客慢车
32 ^(6(20[1-9]|2[1-9]\d|[3-9]\d{2})|7([0-4]\d{2}|5([0-8]\d|9[0-8])))$ 管内普通旅客慢车
33 ^(8([0-8]\d{2}|9[0-8]\d|99[0-8])|7(60[1-9]|6[1-9]\d|[7-9]\d{2}))$ 通勤列车
34 ^[Yy](500|[1-4]\d{2}|[1-9]\d?)$ 跨局旅游列车
35 ^[Yy](50[1-9]|5[1-9]\d|[6-9]\d{2})$ 管内旅游列车
36 ^[Ss][1-9]\d{0,3}$ 市郊旅客列车
37 ^[Ll](6([0-8]\d{2}|9[0-8]\d|99[0-8])|[1-5]\d{3}|[1-9]\d{0,2})$ 直通临时旅客列车
38 ^[Ll]([7-9]\d{3})$ 管内临时旅客列车
39 ^[Xx](19[0-8]|1[0-8]\d|[1-9]\d?)$ 特快货物班列
40 ^[Xx](39[0-8]|3[0-8]\d|2[1-9]\d|20[1-9])$ 快速货物班列
41 ^[Xx]2(40[1-9]|4[1-9]\d|[5-9]\d{2})$ 直通货物快运列车
42 ^[Xx]([4-9]\d{2}|4[1-9]\d|40[1-9])$ 管内货物快运列车
43 ^[Xx]8\d{3}$ 中欧中亚集装箱班列
44 ^[Xx]9([0-4]\d{2}|500)$ 中亚集装箱班列
45 ^[Xx]9(50[1-9]|5[1-9]\d|[6-9]\d{2})$ 水铁联运班列
46 ^[Xx][1-4]\d{4}$ 加挂零散快运车辆货物列车
47 ^1(000[1-9]|00[1-9]\d|0[1-9]\d{2}|[1-9]\d{3})$ 技术直达列车
48 ^2\d{4}$ 直通货物列车
49 ^3\d{4}$ 区段摘挂列车
50 ^4([0-3]\d{3}|4([0-8]\d{2}|9[0-8]\d|99[0-8]))$ 摘挂列车
51 ^4(500[1-9]|50[1-9]\d|5[1-9]\d{2}|[6-9]\d{3})$ 小运转列车
52 ^6\d{4}$ 自备列车
53 ^70\d{3}$ 超限货物列车
54 ^7([1-6]\d{3}|7([0-8]\d{2}|9[0-8]\d|99[0-8]))$ 重载货物列车
55 ^78\d{3}$ 保温列车
56 ^8(0\d{3}|1([0-8]\d{2}|9[0-8]\d|99[0-8]))$ 普快货物班列
57 ^8(200[1-9]|20[1-9]\d|2[1-9]\d{2}|[34]\d{3})$ 煤炭直达列车
58 ^85\d{3}$ 石油直达列车
59 ^86\d{3}$ 始发直达列车
60 ^87\d{3}$ 空车直达列车
61 ^(90\d{3}|91([0-8]\d{2}|9[0-8]\d|99[0-8]))$ 军用列车
62 ^50\d{3}$ 客车单机
63 ^51\d{3}$ 货车单机
64 ^52\d{3}$ 小运转单机
65 ^5(3\d{3}|4([0-8]\d{2}|9[0-8]\d|99[0-8]))$ 补机列车
66 ^55(300|[0-2]\d{2})$ 普通客货试运转列车
67 ^55(500|30[1-9]|3[1-9]\d|4\d{2})$ 高速动车组试运转列车
68 ^55(50[1-9]|5[1-9]\d|[6-9]\d{2})$ 普通动车组试运转列车
69 ^56\d{3}$ 轻油动车与轨道车
70 ^57\d{3}$ 路用列车
71 ^58(10[1-9]|1[1-9]\d|[2-8]\d{2}|9([0-8]\d|9[0-8]))$ 救援列车
72 ^DJ(400|[1-3]\d{2}|[1-9]\d?)$ 动车组检测列车300直通
73 ^DJ([4-9]\d{2}|40[1-9]|4[1-9]\d)$ 动车组检测列车300管内
74 ^DJ1(400|[0-3]\d{2})$ 动车组检测列车250直通
75 ^DJ1(40[1-9]|4[1-9]\d|[5-9]\d{2})$ 动车组检测列车250管内
76 ^DJ[56]\d{3}$ 直通动车组确认列车
77 ^DJ[78]\d{3}$ 管内动车组确认列车
78 ^[Ff][GDCZTKgdcztk]?\d{1,4}$ 因故折返旅客列车
79 ^0[GDCZTKgdcztk]\d{1,4}$ 回送图定客车底
80 ^00(100|[1-9]\d?)$ 有火回送动车组车底
81 ^00(10[1-9]|1[1-9]\d|2([0-8]\d|9[0-8]))$ 无火回送动车组车底
82 ^00(30[1-9]|3[1-9]\d|4([0-8]\d|9[0-8]))$ 无火回送普速客车底

View File

@@ -534,7 +534,7 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
String(it, StandardCharsets.UTF_8)
} ?: return
Log.d(TAG, "Received data len=${newData.length} preview=${newData.take(50)}")
Log.d(TAG, "Received data len=${newData.length} preview=${newData}")
dataBuffer.append(newData)
@@ -640,7 +640,7 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
private fun processJsonString(jsonStr: String): Boolean {
try {
val jsonObject = JSONObject(jsonStr)
Log.d(TAG, "Parsed JSON len=${jsonStr.length} preview=${jsonStr.take(50)}")
Log.d(TAG, "Parsed JSON len=${jsonStr.length} preview=${jsonStr}")
handler.post {

View 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")
}
}
}
}

View File

@@ -62,9 +62,12 @@ import org.noxylva.lbjconsole.ui.screens.HistoryScreen
import org.noxylva.lbjconsole.ui.screens.MapScreen
import org.noxylva.lbjconsole.ui.screens.SettingsScreen
import org.noxylva.lbjconsole.ui.screens.CardMapView
import org.noxylva.lbjconsole.ui.theme.LBJConsoleTheme
import org.noxylva.lbjconsole.util.LocoInfoUtil
import org.noxylva.lbjconsole.util.TrainTypeUtil
import org.noxylva.lbjconsole.database.AppSettingsRepository
import java.util.*
import androidx.lifecycle.lifecycleScope
import android.bluetooth.le.ScanCallback
@@ -75,7 +78,9 @@ class MainActivity : ComponentActivity() {
private val bleClient by lazy { BLEClient(this) }
private val trainRecordManager by lazy { TrainRecordManager(this) }
private val locoInfoUtil by lazy { LocoInfoUtil(this) }
private val trainTypeUtil by lazy { TrainTypeUtil(this) }
private val notificationService by lazy { NotificationService(this) }
private val appSettingsRepository by lazy { AppSettingsRepository(this) }
private var deviceStatus by mutableStateOf("未连接")
@@ -104,15 +109,14 @@ class MainActivity : ComponentActivity() {
private var historyExpandedStates by mutableStateOf<Map<String, Boolean>>(emptyMap())
private var historyScrollPosition by mutableStateOf(0)
private var historyScrollOffset by mutableStateOf(0)
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 mapZoomLevel by mutableStateOf(10.0)
private var mapRailwayLayerVisible by mutableStateOf(true)
private var settingsScrollPosition by mutableStateOf(0)
private var mergeSettings by mutableStateOf(MergeSettings())
private var targetDeviceName = "LBJReceiver"
private var specifiedDeviceAddress by mutableStateOf<String?>(null)
@@ -120,9 +124,6 @@ class MainActivity : ComponentActivity() {
private var showDisconnectButton by mutableStateOf(false)
private var autoConnectEnabled by mutableStateOf(true)
private val settingsPrefs by lazy { getSharedPreferences("app_settings", Context.MODE_PRIVATE) }
private fun getAppVersion(): String {
return try {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
@@ -182,10 +183,10 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
TrainRecord.initializeLocoTypeUtil(this)
loadSettings()
val permissions = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -196,93 +197,23 @@ class MainActivity : ComponentActivity() {
))
} else {
permissions.addAll(arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
))
}
permissions.addAll(arrayOf(
Manifest.permission.ACCESS_FINE_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())
} else {
startAutoScanAndConnect()
}
requestPermissions.launch(permissions.toTypedArray())
Configuration.getInstance().userAgentValue = packageName
bleClient.setTrainInfoCallback { 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()
if (SettingsActivity.isBackgroundServiceEnabled(this)) {
BackgroundService.startService(this)
}
enableEdgeToEdge()
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = false
}
setContent {
LBJConsoleTheme {
val scope = rememberCoroutineScope()
@@ -294,6 +225,9 @@ class MainActivity : ComponentActivity() {
isScanning = isScanning,
currentTab = currentTab,
onTabChange = { tab ->
if (currentTab == 2 && tab != 2) {
saveSettings()
}
currentTab = tab
saveSettings()
},
@@ -322,7 +256,6 @@ class MainActivity : ComponentActivity() {
Log.d(TAG, "Auto connect enabled: $enabled")
},
latestRecord = latestRecord,
recentRecords = recentRecords,
lastUpdateTime = lastUpdateTime,
@@ -332,10 +265,11 @@ class MainActivity : ComponentActivity() {
},
onClearMonitorLog = {
recentRecords.clear()
latestRecord = null
lastUpdateTime = null
temporaryStatusMessage = null
},
allRecords = trainRecordManager.getMixedRecords(),
mergedRecords = trainRecordManager.getMergedRecords(),
recordCount = trainRecordManager.getRecordCount(),
@@ -355,12 +289,14 @@ class MainActivity : ComponentActivity() {
historyEditMode = historyEditMode,
historySelectedRecords = historySelectedRecords,
historyExpandedStates = historyExpandedStates,
historyMapViewStates = historyCardMapStates,
historyScrollPosition = historyScrollPosition,
historyScrollOffset = historyScrollOffset,
onHistoryStateChange = { editMode, selectedRecords, expandedStates, scrollPosition, scrollOffset ->
onHistoryStateChange = { editMode, selectedRecords, expandedStates, mapStates, scrollPosition, scrollOffset ->
historyEditMode = editMode
historySelectedRecords = selectedRecords
historyExpandedStates = expandedStates
historyCardMapStates = mapStates
historyScrollPosition = scrollPosition
historyScrollOffset = scrollOffset
saveSettings()
@@ -369,6 +305,7 @@ class MainActivity : ComponentActivity() {
settingsScrollPosition = settingsScrollPosition,
onSettingsScrollPositionChange = { position ->
android.util.Log.d(TAG, "Settings scroll position changed: $position")
settingsScrollPosition = position
saveSettings()
},
@@ -427,10 +364,7 @@ class MainActivity : ComponentActivity() {
},
appVersion = getAppVersion(),
locoInfoUtil = locoInfoUtil,
onOpenSettings = {
val intent = Intent(this@MainActivity, SettingsActivity::class.java)
startActivity(intent)
}
trainTypeUtil = trainTypeUtil
)
if (showConnectionDialog) {
@@ -483,7 +417,6 @@ class MainActivity : ComponentActivity() {
}
}
}
}
}
@@ -566,7 +499,7 @@ class MainActivity : ComponentActivity() {
private fun handleTrainInfo(jsonData: JSONObject) {
Log.d(TAG, "Received train data=${jsonData.toString().take(50)}...")
Log.d(TAG, "Received train data=${jsonData.toString()}...")
runOnUiThread {
try {
@@ -741,89 +674,101 @@ class MainActivity : ComponentActivity() {
private fun loadSettings() {
settingsDeviceName = settingsPrefs.getString("device_name", "LBJReceiver") ?: "LBJReceiver"
targetDeviceName = settingsDeviceName
currentTab = settingsPrefs.getInt("current_tab", 0)
historyEditMode = settingsPrefs.getBoolean("history_edit_mode", false)
val selectedRecordsStr = settingsPrefs.getString("history_selected_records", "")
historySelectedRecords = if (selectedRecordsStr.isNullOrEmpty()) {
emptySet()
} else {
selectedRecordsStr.split(",").toSet()
lifecycleScope.launch {
try {
val settings = appSettingsRepository.getSettings()
settingsDeviceName = settings.deviceName
targetDeviceName = settings.deviceName
currentTab = settings.currentTab
historyEditMode = settings.historyEditMode
historySelectedRecords = if (settings.historySelectedRecords.isEmpty()) {
emptySet()
} else {
settings.historySelectedRecords.split(",").toSet()
}
historyExpandedStates = if (settings.historyExpandedStates.isEmpty()) {
emptyMap()
} else {
settings.historyExpandedStates.split(";").mapNotNull { pair ->
val parts = pair.split(":")
if (parts.size == 2) parts[0] to (parts[1] == "true") else null
}.toMap()
}
historyScrollPosition = settings.historyScrollPosition
historyScrollOffset = settings.historyScrollOffset
settingsScrollPosition = settings.settingsScrollPosition
android.util.Log.d(TAG, "Loaded settings scroll position: $settingsScrollPosition")
mapCenterPosition = if (settings.mapCenterLat != null && settings.mapCenterLon != null) {
settings.mapCenterLat.toDouble() to settings.mapCenterLon.toDouble()
} else null
mapZoomLevel = settings.mapZoomLevel.toDouble()
mapRailwayLayerVisible = settings.mapRailwayLayerVisible
mergeSettings = trainRecordManager.mergeSettings
specifiedDeviceAddress = settings.specifiedDeviceAddress
searchOrderList = if (settings.searchOrderList.isEmpty()) {
emptyList()
} else {
settings.searchOrderList.split(",").filter { it.isNotBlank() }
}
autoConnectEnabled = settings.autoConnectEnabled
bleClient.setSpecifiedDeviceAddress(specifiedDeviceAddress)
Log.d(TAG, "Loaded settings from Room: deviceName=${settingsDeviceName} tab=${currentTab} specifiedDevice=${specifiedDeviceAddress} searchOrder=${searchOrderList.size} autoConnect=${autoConnectEnabled}")
} catch (e: Exception) {
Log.e(TAG, "Error loading settings from Room", e)
}
}
val expandedStatesStr = settingsPrefs.getString("history_expanded_states", "")
historyExpandedStates = if (expandedStatesStr.isNullOrEmpty()) {
emptyMap()
} else {
expandedStatesStr.split(";").mapNotNull { pair ->
val parts = pair.split(":")
if (parts.size == 2) parts[0] to (parts[1] == "true") else null
}.toMap()
}
historyScrollPosition = settingsPrefs.getInt("history_scroll_position", 0)
historyScrollOffset = settingsPrefs.getInt("history_scroll_offset", 0)
settingsScrollPosition = settingsPrefs.getInt("settings_scroll_position", 0)
val centerLat = settingsPrefs.getFloat("map_center_lat", Float.NaN)
val centerLon = settingsPrefs.getFloat("map_center_lon", Float.NaN)
mapCenterPosition = if (!centerLat.isNaN() && !centerLon.isNaN()) {
centerLat.toDouble() to centerLon.toDouble()
} else null
mapZoomLevel = settingsPrefs.getFloat("map_zoom_level", 10.0f).toDouble()
mapRailwayLayerVisible = settingsPrefs.getBoolean("map_railway_visible", true)
mergeSettings = trainRecordManager.mergeSettings
specifiedDeviceAddress = settingsPrefs.getString("specified_device_address", null)
val searchOrderStr = settingsPrefs.getString("search_order_list", "")
searchOrderList = if (searchOrderStr.isNullOrEmpty()) {
emptyList()
} else {
searchOrderStr.split(",").filter { it.isNotBlank() }
}
autoConnectEnabled = settingsPrefs.getBoolean("auto_connect_enabled", true)
bleClient.setSpecifiedDeviceAddress(specifiedDeviceAddress)
Log.d(TAG, "Loaded settings deviceName=${settingsDeviceName} tab=${currentTab} specifiedDevice=${specifiedDeviceAddress} searchOrder=${searchOrderList.size} autoConnect=${autoConnectEnabled}")
}
private fun saveSettings() {
lifecycleScope.launch(Dispatchers.IO) {
val editor = settingsPrefs.edit()
.putString("device_name", settingsDeviceName)
.putInt("current_tab", currentTab)
.putBoolean("history_edit_mode", historyEditMode)
.putString("history_selected_records", historySelectedRecords.joinToString(","))
.putString("history_expanded_states", historyExpandedStates.map { "${it.key}:${it.value}" }.joinToString(";"))
.putInt("history_scroll_position", historyScrollPosition)
.putInt("history_scroll_offset", historyScrollOffset)
.putInt("settings_scroll_position", settingsScrollPosition)
.putFloat("map_zoom_level", mapZoomLevel.toFloat())
.putBoolean("map_railway_visible", mapRailwayLayerVisible)
.putString("specified_device_address", specifiedDeviceAddress)
.putString("search_order_list", searchOrderList.joinToString(","))
.putBoolean("auto_connect_enabled", autoConnectEnabled)
try {
val currentSettings = appSettingsRepository.getSettings()
val updatedSettings = currentSettings.copy(
deviceName = settingsDeviceName,
currentTab = currentTab,
historyEditMode = historyEditMode,
historySelectedRecords = historySelectedRecords.joinToString(","),
historyExpandedStates = historyExpandedStates.map { "${it.key}:${it.value}" }.joinToString(";"),
historyScrollPosition = historyScrollPosition,
historyScrollOffset = historyScrollOffset,
settingsScrollPosition = settingsScrollPosition,
mapCenterLat = mapCenterPosition?.first?.toFloat(),
mapCenterLon = mapCenterPosition?.second?.toFloat(),
mapZoomLevel = mapZoomLevel.toFloat(),
mapRailwayLayerVisible = mapRailwayLayerVisible,
specifiedDeviceAddress = specifiedDeviceAddress,
searchOrderList = searchOrderList.joinToString(","),
autoConnectEnabled = autoConnectEnabled
)
mapCenterPosition?.let { (lat, lon) ->
editor.putFloat("map_center_lat", lat.toFloat())
editor.putFloat("map_center_lon", lon.toFloat())
appSettingsRepository.saveSettings(updatedSettings)
Log.d(TAG, "Saved settings to Room: deviceName=${settingsDeviceName} tab=${currentTab} settingsScrollPosition=${settingsScrollPosition} mapCenter=${mapCenterPosition} zoom=${mapZoomLevel}")
} catch (e: Exception) {
Log.e(TAG, "Error saving settings to Room", e)
}
editor.apply()
Log.d(TAG, "Saved settings deviceName=${settingsDeviceName} tab=${currentTab} mapCenter=${mapCenterPosition} zoom=${mapZoomLevel}")
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Log.d(TAG, "onNewIntent called")
currentTab = 0
forceUiRefresh()
}
override fun onResume() {
super.onResume()
Log.d(TAG, "App resumed")
@@ -909,14 +854,16 @@ fun MainContent(
locoInfoUtil: LocoInfoUtil,
trainTypeUtil: TrainTypeUtil,
historyEditMode: Boolean,
historySelectedRecords: Set<String>,
historyExpandedStates: Map<String, Boolean>,
historyMapViewStates: Map<String, CardMapView>,
historyScrollPosition: Int,
historyScrollOffset: Int,
onHistoryStateChange: (Boolean, Set<String>, Map<String, Boolean>, Int, Int) -> Unit,
onHistoryStateChange: (Boolean, Set<String>, Map<String, Boolean>, Map<String, CardMapView>, Int, Int) -> Unit,
settingsScrollPosition: Int,
@@ -926,9 +873,7 @@ fun MainContent(
mapCenterPosition: Pair<Double, Double>?,
mapZoomLevel: Double,
mapRailwayLayerVisible: Boolean,
onMapStateChange: (Pair<Double, Double>?, Double, Boolean) -> Unit,
onOpenSettings: () -> Unit
onMapStateChange: (Pair<Double, Double>?, Double, Boolean) -> Unit
) {
val statusColor = if (isConnected) Color(0xFF4CAF50) else Color(0xFFFF5722)
@@ -1022,7 +967,7 @@ fun MainContent(
},
navigationIcon = {
IconButton(onClick = {
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset)
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyMapViewStates, historyScrollPosition, historyScrollOffset)
}) {
Icon(
imageVector = Icons.Default.Close,
@@ -1068,7 +1013,7 @@ fun MainContent(
}
onDeleteRecords(recordsToDelete.toList())
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset)
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyMapViewStates, historyScrollPosition, historyScrollOffset)
}
}
) {
@@ -1124,6 +1069,7 @@ fun MainContent(
lastUpdateTime = lastUpdateTime,
temporaryStatusMessage = temporaryStatusMessage,
locoInfoUtil = locoInfoUtil,
trainTypeUtil = trainTypeUtil,
mergeSettings = mergeSettings,
onClearRecords = onClearRecords,
onRecordClick = onRecordClick,
@@ -1132,6 +1078,7 @@ fun MainContent(
editMode = historyEditMode,
selectedRecords = historySelectedRecords,
expandedStates = historyExpandedStates,
mapViewStates = historyMapViewStates,
scrollPosition = historyScrollPosition,
scrollOffset = historyScrollOffset,
onStateChange = onHistoryStateChange

View File

@@ -5,7 +5,9 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import kotlinx.coroutines.runBlocking
import org.noxylva.lbjconsole.database.AppSettingsRepository
import org.noxylva.lbjconsole.database.TrainDatabase
import android.os.Build
import android.util.Log
import android.view.View
@@ -27,7 +29,7 @@ class NotificationService(private val context: Context) {
}
private val notificationManager = NotificationManagerCompat.from(context)
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private val appSettingsRepository = AppSettingsRepository(context)
private var notificationIdCounter = NOTIFICATION_ID_BASE
init {
@@ -53,11 +55,15 @@ class NotificationService(private val context: Context) {
}
fun isNotificationEnabled(): Boolean {
return prefs.getBoolean(KEY_ENABLED, false)
return runBlocking {
appSettingsRepository.getSettings().notificationEnabled
}
}
fun setNotificationEnabled(enabled: Boolean) {
prefs.edit().putBoolean(KEY_ENABLED, enabled).apply()
runBlocking {
appSettingsRepository.updateNotificationEnabled(enabled)
}
Log.d(TAG, "Notification enabled set to: $enabled")
}
@@ -86,7 +92,7 @@ class NotificationService(private val context: Context) {
context,
0,
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)
@@ -139,7 +145,7 @@ class NotificationService(private val context: Context) {
}
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)
} else {
remoteViews.setViewVisibility(R.id.notification_position, View.GONE)

View File

@@ -1,63 +0,0 @@
package org.noxylva.lbjconsole
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.widget.Switch
import androidx.appcompat.app.AppCompatActivity
class SettingsActivity : AppCompatActivity() {
companion object {
private const val PREFS_NAME = "lbj_console_settings"
private const val KEY_BACKGROUND_SERVICE = "background_service_enabled"
fun isBackgroundServiceEnabled(context: Context): Boolean {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(KEY_BACKGROUND_SERVICE, false)
}
fun setBackgroundServiceEnabled(context: Context, enabled: Boolean) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().putBoolean(KEY_BACKGROUND_SERVICE, enabled).apply()
}
}
private lateinit var backgroundServiceSwitch: Switch
private lateinit var prefs: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.title = "Settings"
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
initViews()
setupListeners()
}
private fun initViews() {
backgroundServiceSwitch = findViewById(R.id.switch_background_service)
backgroundServiceSwitch.isChecked = isBackgroundServiceEnabled(this)
}
private fun setupListeners() {
backgroundServiceSwitch.setOnCheckedChangeListener { _, isChecked ->
setBackgroundServiceEnabled(this, isChecked)
if (isChecked) {
BackgroundService.startService(this)
} else {
BackgroundService.stopService(this)
}
}
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
}

View File

@@ -0,0 +1,31 @@
package org.noxylva.lbjconsole.database
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface AppSettingsDao {
@Query("SELECT * FROM app_settings WHERE id = 1")
suspend fun getSettings(): AppSettingsEntity?
@Query("SELECT * FROM app_settings WHERE id = 1")
fun getSettingsFlow(): Flow<AppSettingsEntity?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSettings(settings: AppSettingsEntity)
@Update
suspend fun updateSettings(settings: AppSettingsEntity)
@Query("DELETE FROM app_settings")
suspend fun deleteAllSettings()
@Query("UPDATE app_settings SET notificationEnabled = :enabled WHERE id = 1")
suspend fun updateNotificationEnabled(enabled: Boolean)
@Transaction
suspend fun saveSettings(settings: AppSettingsEntity) {
insertSettings(settings)
}
}

View File

@@ -0,0 +1,26 @@
package org.noxylva.lbjconsole.database
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "app_settings")
data class AppSettingsEntity(
@PrimaryKey val id: Int = 1,
val deviceName: String = "LBJReceiver",
val currentTab: Int = 0,
val historyEditMode: Boolean = false,
val historySelectedRecords: String = "",
val historyExpandedStates: String = "",
val historyScrollPosition: Int = 0,
val historyScrollOffset: Int = 0,
val settingsScrollPosition: Int = 0,
val mapCenterLat: Float? = null,
val mapCenterLon: Float? = null,
val mapZoomLevel: Float = 10.0f,
val mapRailwayLayerVisible: Boolean = true,
val specifiedDeviceAddress: String? = null,
val searchOrderList: String = "",
val autoConnectEnabled: Boolean = true,
val backgroundServiceEnabled: Boolean = false,
val notificationEnabled: Boolean = false
)

View File

@@ -0,0 +1,127 @@
package org.noxylva.lbjconsole.database
import android.content.Context
import android.content.SharedPreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
class AppSettingsRepository(private val context: Context) {
private val dao = TrainDatabase.getDatabase(context).appSettingsDao()
private val sharedPrefs: SharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
suspend fun getSettings(): AppSettingsEntity {
var settings = dao.getSettings()
if (settings == null) {
settings = migrateFromSharedPreferences()
dao.saveSettings(settings)
}
return settings
}
fun getSettingsFlow(): Flow<AppSettingsEntity?> {
return dao.getSettingsFlow()
}
suspend fun saveSettings(settings: AppSettingsEntity) {
dao.saveSettings(settings)
}
suspend fun updateDeviceName(deviceName: String) {
val current = getSettings()
saveSettings(current.copy(deviceName = deviceName))
}
suspend fun updateCurrentTab(tab: Int) {
val current = getSettings()
saveSettings(current.copy(currentTab = tab))
}
suspend fun updateHistoryEditMode(editMode: Boolean) {
val current = getSettings()
saveSettings(current.copy(historyEditMode = editMode))
}
suspend fun updateHistorySelectedRecords(selectedRecords: String) {
val current = getSettings()
saveSettings(current.copy(historySelectedRecords = selectedRecords))
}
suspend fun updateHistoryExpandedStates(expandedStates: String) {
val current = getSettings()
saveSettings(current.copy(historyExpandedStates = expandedStates))
}
suspend fun updateHistoryScrollPosition(position: Int, offset: Int = 0) {
val current = getSettings()
saveSettings(current.copy(historyScrollPosition = position, historyScrollOffset = offset))
}
suspend fun updateSettingsScrollPosition(position: Int) {
val current = getSettings()
saveSettings(current.copy(settingsScrollPosition = position))
}
suspend fun updateMapSettings(centerLat: Float?, centerLon: Float?, zoomLevel: Float, railwayLayerVisible: Boolean) {
val current = getSettings()
saveSettings(current.copy(
mapCenterLat = centerLat,
mapCenterLon = centerLon,
mapZoomLevel = zoomLevel,
mapRailwayLayerVisible = railwayLayerVisible
))
}
suspend fun updateSpecifiedDeviceAddress(address: String?) {
val current = getSettings()
saveSettings(current.copy(specifiedDeviceAddress = address))
}
suspend fun updateSearchOrderList(orderList: String) {
val current = getSettings()
saveSettings(current.copy(searchOrderList = orderList))
}
suspend fun updateAutoConnectEnabled(enabled: Boolean) {
val current = getSettings()
saveSettings(current.copy(autoConnectEnabled = enabled))
}
suspend fun updateBackgroundServiceEnabled(enabled: Boolean) {
val current = getSettings()
saveSettings(current.copy(backgroundServiceEnabled = enabled))
}
suspend fun updateNotificationEnabled(enabled: Boolean) {
val current = getSettings()
saveSettings(current.copy(notificationEnabled = enabled))
}
private fun migrateFromSharedPreferences(): AppSettingsEntity {
return AppSettingsEntity(
deviceName = sharedPrefs.getString("device_name", "LBJReceiver") ?: "LBJReceiver",
currentTab = sharedPrefs.getInt("current_tab", 0),
historyEditMode = sharedPrefs.getBoolean("history_edit_mode", false),
historySelectedRecords = sharedPrefs.getString("history_selected_records", "") ?: "",
historyExpandedStates = sharedPrefs.getString("history_expanded_states", "") ?: "",
historyScrollPosition = sharedPrefs.getInt("history_scroll_position", 0),
historyScrollOffset = sharedPrefs.getInt("history_scroll_offset", 0),
settingsScrollPosition = sharedPrefs.getInt("settings_scroll_position", 0),
mapCenterLat = if (sharedPrefs.contains("map_center_lat")) sharedPrefs.getFloat("map_center_lat", 0f) else null,
mapCenterLon = if (sharedPrefs.contains("map_center_lon")) sharedPrefs.getFloat("map_center_lon", 0f) else null,
mapZoomLevel = sharedPrefs.getFloat("map_zoom_level", 10.0f),
mapRailwayLayerVisible = sharedPrefs.getBoolean("map_railway_layer_visible", true),
specifiedDeviceAddress = sharedPrefs.getString("specified_device_address", null),
searchOrderList = sharedPrefs.getString("search_order_list", "") ?: "",
autoConnectEnabled = sharedPrefs.getBoolean("auto_connect_enabled", true),
backgroundServiceEnabled = sharedPrefs.getBoolean("background_service_enabled", false),
notificationEnabled = context.getSharedPreferences("notification_settings", Context.MODE_PRIVATE)
.getBoolean("notifications_enabled", false)
)
}
suspend fun clearSharedPreferences() {
sharedPrefs.edit().clear().apply()
}
}

View File

@@ -0,0 +1,145 @@
package org.noxylva.lbjconsole.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
@Database(
entities = [TrainRecordEntity::class, AppSettingsEntity::class],
version = 4,
exportSchema = false
)
abstract class TrainDatabase : RoomDatabase() {
abstract fun trainRecordDao(): TrainRecordDao
abstract fun appSettingsDao(): AppSettingsDao
companion object {
@Volatile
private var INSTANCE: TrainDatabase? = null
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
CREATE TABLE IF NOT EXISTS `app_settings` (
`id` INTEGER NOT NULL,
`deviceName` TEXT NOT NULL,
`currentTab` INTEGER NOT NULL,
`historyEditMode` INTEGER NOT NULL,
`historySelectedRecords` TEXT NOT NULL,
`historyExpandedStates` TEXT NOT NULL,
`historyScrollPosition` INTEGER NOT NULL,
`historyScrollOffset` INTEGER NOT NULL,
`settingsScrollPosition` INTEGER NOT NULL,
`mapCenterLat` REAL,
`mapCenterLon` REAL,
`mapZoomLevel` REAL NOT NULL,
`mapRailwayLayerVisible` INTEGER NOT NULL,
`specifiedDeviceAddress` TEXT,
`searchOrderList` TEXT NOT NULL,
`autoConnectEnabled` INTEGER NOT NULL,
`backgroundServiceEnabled` INTEGER NOT NULL,
PRIMARY KEY(`id`)
)
""")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE app_settings ADD COLUMN notificationEnabled INTEGER NOT NULL DEFAULT 0")
}
}
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 {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TrainDatabase::class.java,
"train_database"
).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4).build()
INSTANCE = instance
instance
}
}
}
}

View File

@@ -0,0 +1,50 @@
package org.noxylva.lbjconsole.database
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface TrainRecordDao {
@Query("SELECT * FROM train_records ORDER BY timestamp DESC")
suspend fun getAllRecords(): List<TrainRecordEntity>
@Query("SELECT * FROM train_records ORDER BY timestamp DESC")
fun getAllRecordsFlow(): Flow<List<TrainRecordEntity>>
@Query("SELECT * FROM train_records WHERE uniqueId = :uniqueId")
suspend fun getRecordById(uniqueId: String): TrainRecordEntity?
@Query("SELECT COUNT(*) FROM train_records")
suspend fun getRecordCount(): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecord(record: TrainRecordEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecords(records: List<TrainRecordEntity>)
@Delete
suspend fun deleteRecord(record: TrainRecordEntity)
@Delete
suspend fun deleteRecords(records: List<TrainRecordEntity>)
@Query("DELETE FROM train_records")
suspend fun deleteAllRecords()
@Query("DELETE FROM train_records WHERE uniqueId = :uniqueId")
suspend fun deleteRecordById(uniqueId: String)
@Query("DELETE FROM train_records WHERE uniqueId IN (:uniqueIds)")
suspend fun deleteRecordsByIds(uniqueIds: List<String>)
@Query("SELECT * FROM train_records WHERE train LIKE '%' || :train || '%' AND route LIKE '%' || :route || '%' AND (:direction = '全部' OR (:direction = '上行' AND direction = 3) OR (:direction = '下行' AND direction = 1)) ORDER BY timestamp DESC")
suspend fun getFilteredRecords(train: String, route: String, direction: String): List<TrainRecordEntity>
@Query("SELECT * FROM train_records ORDER BY timestamp DESC LIMIT :limit")
suspend fun getLatestRecords(limit: Int): List<TrainRecordEntity>
@Query("SELECT * FROM train_records WHERE timestamp >= :fromTime ORDER BY timestamp DESC")
suspend fun getRecordsFromTime(fromTime: Long): List<TrainRecordEntity>
}

View File

@@ -0,0 +1,66 @@
package org.noxylva.lbjconsole.database
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.noxylva.lbjconsole.model.TrainRecord
import org.json.JSONObject
import java.util.*
@Entity(tableName = "train_records")
data class TrainRecordEntity(
@PrimaryKey val uniqueId: String,
val timestamp: Long,
val receivedTimestamp: Long,
val train: String,
val direction: Int,
val speed: String,
val position: String,
val time: String,
val loco: String,
val locoType: String,
val lbjClass: String,
val route: String,
val positionInfo: String,
val rssi: Double
) {
fun toTrainRecord(): TrainRecord {
val jsonData = JSONObject().apply {
put("uniqueId", uniqueId)
put("timestamp", timestamp)
put("receivedTimestamp", receivedTimestamp)
put("train", train)
put("dir", direction)
put("speed", speed)
put("pos", position)
put("time", time)
put("loco", loco)
put("loco_type", locoType)
put("lbj_class", lbjClass)
put("route", route)
put("position_info", positionInfo)
put("rssi", rssi)
}
return TrainRecord(jsonData)
}
companion object {
fun fromTrainRecord(record: TrainRecord): TrainRecordEntity {
return TrainRecordEntity(
uniqueId = record.uniqueId,
timestamp = record.timestamp.time,
receivedTimestamp = record.receivedTimestamp.time,
train = record.train,
direction = record.direction,
speed = record.speed,
position = record.position,
time = record.time,
loco = record.loco,
locoType = record.locoType,
lbjClass = record.lbjClass,
route = record.route,
positionInfo = record.positionInfo,
rssi = record.rssi
)
}
}
}

View File

@@ -1,20 +1,29 @@
package org.noxylva.lbjconsole.model
import android.content.Context
import android.util.Log
import org.json.JSONObject
import java.util.*
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) {
companion object {
const val TAG = "TrainRecord"
private var nextId = 0L
private var LocoTypeUtil: LocoTypeUtil? = null
@Synchronized
private fun generateUniqueId(): String {
return "${System.currentTimeMillis()}_${++nextId}"
}
fun initializeLocoTypeUtil(context: Context) {
if (LocoTypeUtil == null) {
LocoTypeUtil = LocoTypeUtil(context)
}
}
}
val uniqueId: String
@@ -75,20 +84,25 @@ class TrainRecord(jsonData: JSONObject? = null) {
position = jsonData.optString("pos", "")
time = jsonData.optString("time", "")
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", "")
route = jsonData.optString("route", "")
positionInfo = jsonData.optString("position_info", "")
rssi = jsonData.optDouble("rssi", 0.0)
_coordinates = null
Log.d(TAG, "Successfully parsed: train=$train, dir=$direction, speed=$speed")
Log.d(TAG, "Successfully parsed: train=$train, dir=$direction, speed=$speed, lbjClass='$lbjClass', locoType='$locoType'")
} catch (e: Exception) {
Log.e(TAG, "JSON parse error: ${e.message}", e)
try { train = jsonData.optString("train", "") } catch (e: Exception) { }
try { direction = jsonData.optInt("dir", 0) } 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
}
private fun isValidValue(value: String): Boolean {
@@ -134,7 +148,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
lbjClass.trim()
} else if (isValidValue(train)) {
train.trim()
} else ""
} else null
val map = mutableMapOf<String, String>()
@@ -143,14 +157,17 @@ class TrainRecord(jsonData: JSONObject? = null) {
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 (isValidValue(speed)) map["speed"] = "速度: ${speed.trim()} km/h"
if (isValidValue(position)) map["position"] = "位置: ${position.trim()} km"
if (isValidValue(speed)) map["speed"] = "${speed.trim()} km/h"
if (isValidValue(position)) {
map["position"] = "${position.trim().removeSuffix(".")} K"
}
val timeToDisplay = if (showDetailedTime) {
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
if (isValidValue(time)) {
"列车时间: $time\n接收时间: ${dateFormat.format(receivedTimestamp)}"
"$time\n${dateFormat.format(receivedTimestamp)}"
} else {
dateFormat.format(receivedTimestamp)
}
@@ -164,13 +181,13 @@ class TrainRecord(jsonData: JSONObject? = null) {
}
}
map["time"] = timeToDisplay
if (isValidValue(loco)) map["loco"] = "机车号: ${loco.trim()}"
if (isValidValue(locoType)) map["loco_type"] = "型号: ${locoType.trim()}"
if (isValidValue(route)) map["route"] = "线路: ${route.trim()}"
if (isValidValue(loco)) map["loco"] = "${loco.trim()}"
if (isValidValue(locoType)) map["loco_type"] = "${locoType.trim()}"
if (isValidValue(route)) map["route"] = "${route.trim()}"
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
}

View File

@@ -7,6 +7,8 @@ import android.util.Log
import kotlinx.coroutines.*
import org.json.JSONArray
import org.json.JSONObject
import org.noxylva.lbjconsole.database.TrainDatabase
import org.noxylva.lbjconsole.database.TrainRecordEntity
import java.io.File
import java.io.FileWriter
import java.text.SimpleDateFormat
@@ -27,6 +29,8 @@ class TrainRecordManager(private val context: Context) {
private val trainRecords = CopyOnWriteArrayList<TrainRecord>()
private val recordCount = AtomicInteger(0)
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private val database = TrainDatabase.getDatabase(context)
private val trainRecordDao = database.trainRecordDao()
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
var mergeSettings = MergeSettings()
@@ -34,11 +38,36 @@ class TrainRecordManager(private val context: Context) {
init {
ioScope.launch {
migrateFromSharedPreferences()
loadRecords()
loadMergeSettings()
}
}
private suspend fun migrateFromSharedPreferences() {
try {
val jsonStr = prefs.getString(KEY_RECORDS, null)
if (jsonStr != null && jsonStr != "[]") {
val jsonArray = JSONArray(jsonStr)
val records = mutableListOf<TrainRecordEntity>()
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
val trainRecord = TrainRecord(jsonObject)
records.add(TrainRecordEntity.fromTrainRecord(trainRecord))
}
if (records.isNotEmpty()) {
trainRecordDao.insertRecords(records)
prefs.edit().remove(KEY_RECORDS).apply()
Log.d(TAG, "Migrated ${records.size} records from SharedPreferences to Room database")
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to migrate records: ${e.message}")
}
}
private var filterTrain: String = ""
private var filterRoute: String = ""
@@ -52,11 +81,16 @@ class TrainRecordManager(private val context: Context) {
while (trainRecords.size > MAX_RECORDS) {
trainRecords.removeAt(trainRecords.size - 1)
val removedRecord = trainRecords.removeAt(trainRecords.size - 1)
ioScope.launch {
trainRecordDao.deleteRecordById(removedRecord.uniqueId)
}
}
recordCount.incrementAndGet()
saveRecords()
ioScope.launch {
trainRecordDao.insertRecord(TrainRecordEntity.fromTrainRecord(record))
}
return record
}
@@ -76,6 +110,16 @@ class TrainRecordManager(private val context: Context) {
}
}
suspend fun getFilteredRecordsFromDatabase(): List<TrainRecord> {
return try {
val entities = trainRecordDao.getFilteredRecords(filterTrain, filterRoute, filterDirection)
entities.map { it.toTrainRecord() }
} catch (e: Exception) {
Log.e(TAG, "Failed to get filtered records from database: ${e.message}")
emptyList()
}
}
private fun matchFilter(record: TrainRecord): Boolean {
@@ -118,32 +162,56 @@ class TrainRecordManager(private val context: Context) {
}
suspend fun refreshRecordsFromDatabase() {
try {
val entities = trainRecordDao.getAllRecords()
trainRecords.clear()
entities.forEach { entity ->
trainRecords.add(entity.toTrainRecord())
}
recordCount.set(trainRecords.size)
Log.d(TAG, "Refreshed ${trainRecords.size} records from database")
} catch (e: Exception) {
Log.e(TAG, "Failed to refresh records from database: ${e.message}")
}
}
fun clearRecords() {
trainRecords.clear()
recordCount.set(0)
saveRecords()
ioScope.launch {
trainRecordDao.deleteAllRecords()
}
}
fun deleteRecord(record: TrainRecord): Boolean {
val result = trainRecords.remove(record)
if (result) {
recordCount.decrementAndGet()
saveRecords()
ioScope.launch {
trainRecordDao.deleteRecordById(record.uniqueId)
}
}
return result
}
fun deleteRecords(records: List<TrainRecord>): Int {
var deletedCount = 0
val idsToDelete = mutableListOf<String>()
records.forEach { record ->
if (trainRecords.remove(record)) {
deletedCount++
idsToDelete.add(record.uniqueId)
}
}
if (deletedCount > 0) {
recordCount.addAndGet(-deletedCount)
saveRecords()
ioScope.launch {
trainRecordDao.deleteRecordsByIds(idsToDelete)
}
}
return deletedCount
}
@@ -151,12 +219,9 @@ class TrainRecordManager(private val context: Context) {
private fun saveRecords() {
ioScope.launch {
try {
val jsonArray = JSONArray()
for (record in trainRecords) {
jsonArray.put(record.toJSON())
}
prefs.edit().putString(KEY_RECORDS, jsonArray.toString()).apply()
Log.d(TAG, "Saved ${trainRecords.size} records")
val entities = trainRecords.map { TrainRecordEntity.fromTrainRecord(it) }
trainRecordDao.insertRecords(entities)
Log.d(TAG, "Saved ${trainRecords.size} records to database")
} catch (e: Exception) {
Log.e(TAG, "Failed to save records: ${e.message}")
}
@@ -164,19 +229,17 @@ class TrainRecordManager(private val context: Context) {
}
private fun loadRecords() {
private suspend fun loadRecords() {
try {
val jsonStr = prefs.getString(KEY_RECORDS, "[]")
val jsonArray = JSONArray(jsonStr)
val entities = trainRecordDao.getAllRecords()
trainRecords.clear()
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
trainRecords.add(TrainRecord(jsonObject))
entities.forEach { entity ->
trainRecords.add(entity.toTrainRecord())
}
recordCount.set(trainRecords.size)
Log.d(TAG, "Loaded ${trainRecords.size} records")
Log.d(TAG, "Loaded ${trainRecords.size} records from database")
} catch (e: Exception) {
Log.e(TAG, "Failed to load records: ${e.message}")
}
@@ -234,12 +297,12 @@ class TrainRecordManager(private val context: Context) {
}
private fun processRecordsForMerging(records: List<TrainRecord>, settings: MergeSettings): List<MergedTrainRecord> {
val currentTime = Date()
val validRecords = records.filter { record ->
settings.timeWindow.seconds?.let { windowSeconds ->
val validRecords = settings.timeWindow.seconds?.let { windowSeconds ->
val currentTime = Date()
records.filter { record ->
(currentTime.time - record.timestamp.time) / 1000 <= windowSeconds
} ?: true
}
}
} ?: records
return when (settings.groupBy) {
GroupBy.TRAIN_OR_LOCO -> processTrainOrLocoMerging(validRecords)
@@ -254,11 +317,14 @@ class TrainRecordManager(private val context: Context) {
groupedRecords.mapNotNull { (groupKey, groupRecords) ->
if (groupRecords.size >= 2) {
val sortedRecords = groupRecords.sortedBy { it.timestamp }
val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!!
val latestRecord = if (groupRecords.size > 1) {
groupRecords.maxByOrNull { it.timestamp } ?: groupRecords.last()
} else {
groupRecords.last()
}
MergedTrainRecord(
groupKey = groupKey,
records = sortedRecords,
records = groupRecords.toList(),
latestRecord = latestRecord
)
} else null
@@ -268,7 +334,9 @@ class TrainRecordManager(private val context: Context) {
}
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 ->
val train = record.train.trim()
@@ -278,38 +346,44 @@ class TrainRecordManager(private val context: Context) {
return@forEach
}
var foundGroup: MutableList<TrainRecord>? = null
var targetGroup: MutableList<TrainRecord>? = null
for (group in groups) {
val shouldMerge = group.any { existingRecord ->
val existingTrain = existingRecord.train.trim()
val existingLoco = existingRecord.loco.trim()
(train.isNotEmpty() && train != "<NUL>" && train == existingTrain) ||
(loco.isNotEmpty() && loco != "<NUL>" && loco == existingLoco)
}
if (shouldMerge) {
foundGroup = group
break
}
if (train.isNotEmpty() && train != "<NUL>") {
targetGroup = trainGroups[train]
}
if (foundGroup != null) {
foundGroup.add(record)
if (targetGroup == null && loco.isNotEmpty() && loco != "<NUL>") {
targetGroup = locoGroups[loco]
}
if (targetGroup != null) {
targetGroup.add(record)
if (train.isNotEmpty() && train != "<NUL>") {
trainGroups[train] = targetGroup
}
if (loco.isNotEmpty() && loco != "<NUL>") {
locoGroups[loco] = targetGroup
}
} 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) {
val sortedRecords = groupRecords.sortedBy { it.timestamp }
val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!!
val latestRecord = groupRecords.maxByOrNull { it.timestamp } ?: groupRecords.lastOrNull() ?: return@mapNotNull null
val groupKey = "${latestRecord.train}_OR_${latestRecord.loco}"
MergedTrainRecord(
groupKey = groupKey,
records = sortedRecords,
records = groupRecords.toList(),
latestRecord = latestRecord
)
} else null
@@ -349,4 +423,41 @@ class TrainRecordManager(private val context: Context) {
mergeSettings = MergeSettings()
}
}
suspend fun exportRecordsToJson(): JSONArray {
val jsonArray = JSONArray()
try {
val entities = trainRecordDao.getAllRecords()
entities.forEach { entity ->
val record = entity.toTrainRecord()
jsonArray.put(record.toJSON())
}
Log.d(TAG, "Exported ${entities.size} records to JSON")
} catch (e: Exception) {
Log.e(TAG, "Failed to export records to JSON: ${e.message}")
}
return jsonArray
}
suspend fun importRecordsFromJson(jsonArray: JSONArray): Int {
var importedCount = 0
try {
val records = mutableListOf<TrainRecordEntity>()
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
val trainRecord = TrainRecord(jsonObject)
records.add(TrainRecordEntity.fromTrainRecord(trainRecord))
}
if (records.isNotEmpty()) {
trainRecordDao.insertRecords(records)
importedCount = records.size
refreshRecordsFromDatabase()
Log.d(TAG, "Imported $importedCount records from JSON")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to import records from JSON: ${e.message}")
}
return importedCount
}
}

View File

@@ -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
)
}
}

View File

@@ -42,8 +42,15 @@ import org.noxylva.lbjconsole.model.MergedTrainRecord
import org.noxylva.lbjconsole.model.MergeSettings
import org.noxylva.lbjconsole.model.GroupBy
import org.noxylva.lbjconsole.util.LocoInfoUtil
import org.noxylva.lbjconsole.util.TrainTypeUtil
import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
import java.text.SimpleDateFormat
import java.util.*
import androidx.compose.ui.platform.LocalContext
data class CardMapView(val center: GeoPoint, val zoom: Double)
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -54,6 +61,7 @@ fun TrainRecordItem(
expandedStatesMap: MutableMap<String, Boolean>,
latestRecord: TrainRecord?,
locoInfoUtil: LocoInfoUtil?,
trainTypeUtil: TrainTypeUtil?,
onRecordClick: (TrainRecord) -> Unit,
onToggleSelection: (TrainRecord) -> Unit,
onLongClick: (TrainRecord) -> Unit,
@@ -62,6 +70,8 @@ fun TrainRecordItem(
val recordId = record.uniqueId
val isExpanded = expandedStatesMap[recordId] == true
val cardColor = when {
isSelected -> MaterialTheme.colorScheme.primaryContainer
else -> MaterialTheme.colorScheme.surface
@@ -130,32 +140,53 @@ fun TrainRecordItem(
}
}
Text(
text = "${record.rssi} dBm",
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
val trainType = if (record.train?.trim().isNullOrEmpty()) {
null
} else {
val lbjClassValue = record.lbjClass?.trim() ?: "NA"
trainTypeUtil?.getTrainType(lbjClassValue, record.train!!.trim())
}
if (!trainType.isNullOrEmpty()) {
Text(
text = trainType,
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
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(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
val trainDisplay = recordMap["train"]?.toString() ?: "未知列车"
val trainDisplay = recordMap["train"]?.toString() ?: ""
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = trainDisplay,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = MaterialTheme.colorScheme.primary
)
if (trainDisplay.isNotEmpty()) {
Text(
text = trainDisplay,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = MaterialTheme.colorScheme.primary
)
}
val directionText = when (record.direction) {
1 -> ""
@@ -185,16 +216,16 @@ fun TrainRecordItem(
}
}
}
val formattedInfo = when {
record.locoType.isNotEmpty() && record.loco.isNotEmpty() -> {
val shortLoco = if (record.loco.length > 5) {
record.loco.takeLast(5)
} else {
record.loco
}
"${record.locoType}-${shortLoco}"
val shortLoco = if (record.loco.length > 5) {
record.loco.takeLast(5)
} else {
record.loco
}
"${record.locoType}-${shortLoco}"
}
record.locoType.isNotEmpty() -> record.locoType
record.loco.isNotEmpty() -> record.loco
else -> ""
@@ -209,7 +240,7 @@ fun TrainRecordItem(
}
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(if (shouldShowOnlyTime) 0.dp else 2.dp))
Row(
modifier = Modifier.fillMaxWidth(),
@@ -240,7 +271,7 @@ fun TrainRecordItem(
if (isValidPosition) {
Text(
text = "${position}K",
text = "${position.trim().removeSuffix(".")}K",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.alignByBaseline()
@@ -269,7 +300,7 @@ fun TrainRecordItem(
record.loco
)
if (locoInfoText != null) {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(if (shouldShowOnlyTime) 0.dp else 2.dp))
Text(
text = locoInfoText,
fontSize = 14.sp,
@@ -277,7 +308,8 @@ fun TrainRecordItem(
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(if (shouldShowOnlyTime) 0.dp else 2.dp))
AnimatedVisibility(
visible = isExpanded,
enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)),
@@ -316,6 +348,7 @@ fun TrainRecordItem(
controller.setZoom(10.0)
controller.setCenter(coordinates)
this.isTilesScaledToDpi = true
tilesScaleFactor = context.resources.displayMetrics.density * 0.2f
this.setUseDataConnection(true)
try {
@@ -393,9 +426,12 @@ fun MergedTrainRecordItem(
mergedRecord: MergedTrainRecord,
expandedStatesMap: MutableMap<String, Boolean>,
locoInfoUtil: LocoInfoUtil?,
trainTypeUtil: TrainTypeUtil?,
mergeSettings: MergeSettings? = null,
isInEditMode: Boolean = false,
selectedRecords: List<TrainRecord> = emptyList(),
mapViewState: CardMapView?,
onMapViewStateChange: (CardMapView) -> Unit,
onToggleSelection: (TrainRecord) -> Unit = {},
onLongClick: (TrainRecord) -> Unit = {},
modifier: Modifier = Modifier
@@ -484,11 +520,19 @@ fun MergedTrainRecordItem(
}
}
Text(
text = "${latestRecord.rssi} dBm",
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
val trainType = if (latestRecord.train?.trim().isNullOrEmpty()) {
null
} else {
val lbjClassValue = latestRecord.lbjClass?.trim() ?: "NA"
trainTypeUtil?.getTrainType(lbjClassValue, latestRecord.train!!.trim())
}
if (!trainType.isNullOrEmpty()) {
Text(
text = trainType,
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.height(2.dp))
@@ -498,18 +542,20 @@ fun MergedTrainRecordItem(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
val trainDisplay = recordMap["train"]?.toString() ?: "未知列车"
val trainDisplay = recordMap["train"]?.toString() ?: ""
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = trainDisplay,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = MaterialTheme.colorScheme.primary
)
if (trainDisplay.isNotEmpty()) {
Text(
text = trainDisplay,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = MaterialTheme.colorScheme.primary
)
}
val directionText = when (latestRecord.direction) {
1 -> ""
@@ -542,13 +588,13 @@ fun MergedTrainRecordItem(
val formattedInfo = when {
latestRecord.locoType.isNotEmpty() && latestRecord.loco.isNotEmpty() -> {
val shortLoco = if (latestRecord.loco.length > 5) {
latestRecord.loco.takeLast(5)
} else {
latestRecord.loco
}
"${latestRecord.locoType}-${shortLoco}"
val shortLoco = if (latestRecord.loco.length > 5) {
latestRecord.loco.takeLast(5)
} else {
latestRecord.loco
}
"${latestRecord.locoType}-${shortLoco}"
}
latestRecord.locoType.isNotEmpty() -> latestRecord.locoType
latestRecord.loco.isNotEmpty() -> latestRecord.loco
else -> ""
@@ -563,7 +609,7 @@ fun MergedTrainRecordItem(
}
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(2.dp))
Row(
modifier = Modifier.fillMaxWidth(),
@@ -594,7 +640,7 @@ fun MergedTrainRecordItem(
if (isValidPosition) {
Text(
text = "${position}K",
text = "${position.trim().removeSuffix(".")}K",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.alignByBaseline()
@@ -623,7 +669,7 @@ fun MergedTrainRecordItem(
latestRecord.loco
)
if (locoInfoText != null) {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(2.dp))
Text(
text = locoInfoText,
fontSize = 14.sp,
@@ -631,100 +677,159 @@ fun MergedTrainRecordItem(
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(2.dp))
AnimatedVisibility(
visible = isExpanded,
enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)),
exit = shrinkVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow))
) {
Column {
val coordinates = remember { latestRecord.getCoordinates() }
val allValidCoordinates = remember {
mergedRecord.records
.mapNotNull { it.getCoordinates() }
.filter { it.latitude != 0.0 || it.longitude != 0.0 }
}
if (allValidCoordinates.isNotEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(220.dp)
.padding(vertical = 4.dp)
.clip(RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center
) {
AndroidView(
modifier = Modifier.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {},
factory = { context ->
MapView(context).apply {
setTileSource(TileSourceFactory.MAPNIK)
setMultiTouchControls(true)
zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
isHorizontalMapRepetitionEnabled = false
isVerticalMapRepetitionEnabled = false
setHasTransientState(true)
setOnTouchListener { v, event ->
v.parent?.requestDisallowInterceptTouchEvent(true)
false
}
this.isTilesScaledToDpi = true
tilesScaleFactor = context.resources.displayMetrics.density * 0.2f
this.setUseDataConnection(true)
addMapListener(object : org.osmdroid.events.MapListener {
override fun onScroll(event: org.osmdroid.events.ScrollEvent?): Boolean {
val center = mapCenter
val zoom = zoomLevelDouble
onMapViewStateChange(CardMapView(center as GeoPoint, zoom))
return true
}
if (coordinates != null) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(220.dp)
.padding(vertical = 4.dp)
.clip(RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center
) {
AndroidView(
modifier = Modifier.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {},
factory = { context ->
MapView(context).apply {
setTileSource(TileSourceFactory.MAPNIK)
setMultiTouchControls(true)
zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
isHorizontalMapRepetitionEnabled = false
isVerticalMapRepetitionEnabled = false
setHasTransientState(true)
setOnTouchListener { v, event ->
v.parent?.requestDisallowInterceptTouchEvent(true)
false
}
controller.setZoom(10.0)
controller.setCenter(coordinates)
this.isTilesScaledToDpi = true
this.setUseDataConnection(true)
override fun onZoom(event: org.osmdroid.events.ZoomEvent?): Boolean {
val center = mapCenter
val zoom = zoomLevelDouble
onMapViewStateChange(CardMapView(center as GeoPoint, zoom))
return true
}
})
try {
val railwayTileSource = XYTileSource(
"OpenRailwayMap", 8, 16, 256, ".png",
arrayOf(
"https://a.tiles.openrailwayMap.org/standard/",
"https://b.tiles.openrailwaymap.org/standard/",
"https://c.tiles.openrailwaymap.org/standard/"
),
"© OpenRailwayMap contributors, © OpenStreetMap contributors"
)
try {
val railwayTileSource = XYTileSource(
"OpenRailwayMap", 8, 16, 256, ".png",
arrayOf(
"https://a.tiles.openrailwaymap.org/standard/",
"https://b.tiles.openrailwaymap.org/standard/",
"https://c.tiles.openrailwaymap.org/standard/"
),
"© OpenRailwayMap contributors, © OpenStreetMap contributors"
)
val railwayProvider = MapTileProviderBasic(context)
railwayProvider.tileSource = railwayTileSource
val railwayProvider = MapTileProviderBasic(context)
railwayProvider.tileSource = railwayTileSource
val railwayOverlay = TilesOverlay(railwayProvider, context)
railwayOverlay.loadingBackgroundColor = android.graphics.Color.TRANSPARENT
railwayOverlay.loadingLineColor = android.graphics.Color.TRANSPARENT
val railwayOverlay = TilesOverlay(railwayProvider, context)
railwayOverlay.loadingBackgroundColor = android.graphics.Color.TRANSPARENT
railwayOverlay.loadingLineColor = android.graphics.Color.TRANSPARENT
overlays.add(railwayOverlay)
} catch (e: Exception) {
e.printStackTrace()
}
try {
val locationProvider = GpsMyLocationProvider(context).apply {
locationUpdateMinDistance = 10f
locationUpdateMinTime = 1000
overlays.add(railwayOverlay)
} catch (e: Exception) {
e.printStackTrace()
}
MyLocationNewOverlay(locationProvider, this).apply {
enableMyLocation()
}.also { overlays.add(it) }
} catch (e: Exception) {
e.printStackTrace()
try {
val locationProvider = GpsMyLocationProvider(context).apply {
locationUpdateMinDistance = 10f
locationUpdateMinTime = 1000
}
MyLocationNewOverlay(locationProvider, this).apply {
enableMyLocation()
}.also { overlays.add(it) }
} catch (e: Exception) {
e.printStackTrace()
}
mergedRecord.records.forEach { record ->
record.getCoordinates()?.let { coordinates ->
if (coordinates.latitude != 0.0 || coordinates.longitude != 0.0) {
val recordMap = record.toMap()
val marker = Marker(this)
marker.position = coordinates
val latStr = String.format("%.4f", coordinates.latitude)
val lonStr = String.format("%.4f", coordinates.longitude)
val coordStr = "${latStr}°N, ${lonStr}°E"
marker.title = recordMap["train"]?.toString() ?: "列车"
marker.snippet = coordStr
marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f)
overlays.add(marker)
if (record == latestRecord) {
marker.showInfoWindow()
}
}
}
}
if (mapViewState != null) {
controller.setZoom(mapViewState.zoom)
controller.setCenter(mapViewState.center)
} else if (allValidCoordinates.size > 1) {
val boundingBox = BoundingBox.fromGeoPoints(allValidCoordinates.filter { it.latitude != 0.0 || it.longitude != 0.0 })
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) {
if (width > 0 && height > 0) {
val zoomLevel = org.osmdroid.views.MapView.getTileSystem().getBoundingBoxZoom(boundingBox, width, height)
val latSpan = boundingBox.latitudeSpan
val adjustedCenter = org.osmdroid.util.GeoPoint(
boundingBox.center.latitude + latSpan * 0.25,
boundingBox.center.longitude
)
val newZoom = zoomLevel - 1.0
controller.setZoom(newZoom)
controller.setCenter(adjustedCenter)
onMapViewStateChange(CardMapView(adjustedCenter, newZoom))
removeOnLayoutChangeListener(this)
}
}
}
addOnLayoutChangeListener(layoutListener)
} else if (allValidCoordinates.isNotEmpty()) {
val center = allValidCoordinates.first()
val zoom = 10.0
controller.setZoom(zoom)
controller.setCenter(center)
onMapViewStateChange(CardMapView(center, zoom))
}
}
val marker = Marker(this)
marker.position = coordinates
val latStr = String.format("%.4f", coordinates.latitude)
val lonStr = String.format("%.4f", coordinates.longitude)
val coordStr = "${latStr}°N, ${lonStr}°E"
marker.title = recordMap["train"]?.toString() ?: "列车"
marker.snippet = coordStr
marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f)
overlays.add(marker)
marker.showInfoWindow()
}
},
update = { mapView -> mapView.invalidate() }
)
},
update = { mapView -> mapView.invalidate() }
)
}
}
if (recordMap.containsKey("position_info")) {
@@ -771,6 +876,30 @@ fun MergedTrainRecordItem(
"${recordItem.locoType}-${recordItem.loco}"
} else null
}
GroupBy.TRAIN_OR_LOCO -> {
val latestTrain = mergedRecord.latestRecord.train.trim()
val latestLoco = mergedRecord.latestRecord.loco.trim()
val recordTrain = recordItem.train.trim()
val recordLoco = recordItem.loco.trim()
when {
latestTrain.isNotEmpty() && latestTrain != "<NUL>" &&
recordTrain.isNotEmpty() && recordTrain != "<NUL>" &&
latestTrain == recordTrain && latestLoco != recordLoco -> {
if (recordLoco.isNotEmpty() && recordLoco != "<NUL>") {
"${recordItem.locoType}-${recordLoco}"
} else null
}
latestLoco.isNotEmpty() && latestLoco != "<NUL>" &&
recordLoco.isNotEmpty() && recordLoco != "<NUL>" &&
latestLoco == recordLoco && latestTrain != recordTrain -> {
if (recordTrain.isNotEmpty() && recordTrain != "<NUL>") {
recordTrain
} else null
}
else -> null
}
}
else -> null
}
@@ -794,12 +923,12 @@ fun MergedTrainRecordItem(
}
if (recordItem.position.isNotEmpty() && recordItem.position != "<NUL>") {
if (isNotEmpty()) append(" ")
append("${recordItem.position}K")
append("${recordItem.position.trim().removeSuffix(".")}K")
}
}
Text(
text = locationText.ifEmpty { "位置未知" },
text = locationText.ifEmpty { "" },
fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -849,6 +978,7 @@ fun HistoryScreen(
lastUpdateTime: Date?,
temporaryStatusMessage: String? = null,
locoInfoUtil: LocoInfoUtil? = null,
trainTypeUtil: TrainTypeUtil? = null,
mergeSettings: MergeSettings? = null,
onClearRecords: () -> Unit = {},
onRecordClick: (TrainRecord) -> Unit = {},
@@ -857,9 +987,10 @@ fun HistoryScreen(
editMode: Boolean = false,
selectedRecords: Set<String> = emptySet(),
expandedStates: Map<String, Boolean> = emptyMap(),
mapViewStates: Map<String, CardMapView> = emptyMap(),
scrollPosition: Int = 0,
scrollOffset: Int = 0,
onStateChange: (Boolean, Set<String>, Map<String, Boolean>, Int, Int) -> Unit = { _, _, _, _, _ -> }
onStateChange: (Boolean, Set<String>, Map<String, Boolean>, Map<String, CardMapView>, Int, Int) -> Unit = { _, _, _, _, _, _ -> }
) {
val refreshKey = latestRecord?.timestamp?.time ?: 0
@@ -889,6 +1020,9 @@ fun HistoryScreen(
val expandedStatesMap = remember(expandedStates) {
mutableStateMapOf<String, Boolean>().apply { putAll(expandedStates) }
}
val mapViewStatesMap = remember(mapViewStates) {
mutableStateMapOf<String, CardMapView>().apply { putAll(mapViewStates) }
}
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = scrollPosition,
@@ -919,28 +1053,28 @@ fun HistoryScreen(
LaunchedEffect(isInEditMode, selectedRecordsList.size) {
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), mapViewStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
}
LaunchedEffect(expandedStatesMap.toMap()) {
if (!isInEditMode) {
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
delay(50)
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), mapViewStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
}
}
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
if (!isInEditMode) {
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), mapViewStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
}
}
LaunchedEffect(selectedRecordsList.size) {
if (selectedRecordsList.isEmpty() && isInEditMode) {
isInEditMode = false
onStateChange(false, emptySet(), expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
onStateChange(false, emptySet(), expandedStatesMap.toMap(), mapViewStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
}
}
@@ -1018,6 +1152,7 @@ fun HistoryScreen(
expandedStatesMap = expandedStatesMap,
latestRecord = latestRecord,
locoInfoUtil = locoInfoUtil,
trainTypeUtil = trainTypeUtil,
onRecordClick = onRecordClick,
onToggleSelection = { record ->
if (selectedRecordsList.contains(record)) {
@@ -1041,9 +1176,14 @@ fun HistoryScreen(
mergedRecord = item,
expandedStatesMap = expandedStatesMap,
locoInfoUtil = locoInfoUtil,
trainTypeUtil = trainTypeUtil,
mergeSettings = mergeSettings,
isInEditMode = isInEditMode,
selectedRecords = selectedRecordsList,
mapViewState = mapViewStatesMap[item.groupKey],
onMapViewStateChange = { newState ->
mapViewStatesMap[item.groupKey] = newState
},
onToggleSelection = { record ->
if (selectedRecordsList.contains(record)) {
selectedRecordsList.remove(record)

View File

@@ -7,7 +7,12 @@ import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.util.Log
import android.view.ViewGroup
import androidx.compose.foundation.background
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.filled.Refresh
import androidx.compose.material.icons.filled.MyLocation
@@ -16,11 +21,16 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.LifecycleEventObserver
import kotlinx.coroutines.launch
@@ -112,7 +122,6 @@ fun MapScreen(
val recordMap = record.toMap()
title = recordMap["train"]?.toString() ?: "列车"
val latStr = String.format("%.4f", point.latitude)
val lonStr = String.format("%.4f", point.longitude)
val coordStr = "${latStr}°N, ${lonStr}°E"
@@ -574,8 +583,8 @@ fun Context.getCompactMarkerDrawable(color: Int): Drawable {
private fun Int.directionText(): String = when (this) {
1 -> ""
3 -> ""
1 -> "下行"
3 -> "上行"
else -> "?"
}
@@ -585,50 +594,143 @@ private fun TrainMarkerDialog(
position: GeoPoint?,
onDismiss: () -> Unit
) {
val recordMap = record.toMap()
val displayItems = recordMap.filterKeys {
it !in setOf("train", "direction", "time")
}.toList()
AlertDialog(
onDismissRequest = onDismiss,
title = {
val recordMap = record.toMap()
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = recordMap["train"]?.toString() ?: "列车", style = MaterialTheme.typography.titleLarge)
Text(
text = recordMap["train"]?.toString() ?: "列车",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.weight(1f)
)
recordMap["direction"]?.let { direction ->
Spacer(modifier = Modifier.width(8.dp))
Text(
text = direction,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
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 = {
Column {
record.toMap().forEach { (key, value) ->
if (key != "train" && key != "direction") {
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
}
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 2.dp)
text = title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = value.toString(),
style = MaterialTheme.typography.bodyMedium
)
}
}
position?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "坐标: ${String.format("%.6f", it.latitude)}, ${String.format("%.6f", it.longitude)}",
style = MaterialTheme.typography.bodyMedium
)
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 = {
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
)
}
}
}

View File

@@ -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
)
}
}

View File

@@ -1,5 +1,6 @@
package org.noxylva.lbjconsole.ui.screens
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -15,16 +16,17 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.noxylva.lbjconsole.model.MergeSettings
import org.noxylva.lbjconsole.model.GroupBy
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.NotificationService
import org.noxylva.lbjconsole.FilePickerActivity
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.DisposableEffect
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -44,19 +46,14 @@ fun SettingsScreen(
onAutoConnectEnabledChange: (Boolean) -> Unit = {}
) {
val uriHandler = LocalUriHandler.current
val scrollState = rememberScrollState()
val scrollState = rememberScrollState(initial = scrollPosition)
LaunchedEffect(scrollPosition) {
if (scrollState.value != scrollPosition) {
scrollState.scrollTo(scrollPosition)
DisposableEffect(Unit) {
onDispose {
onScrollPositionChange(scrollState.value)
}
}
LaunchedEffect(scrollState.value) {
delay(50)
onScrollPositionChange(scrollState.value)
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -199,11 +196,15 @@ fun SettingsScreen(
val context = LocalContext.current
val notificationService = remember(context) { NotificationService(context) }
var backgroundServiceEnabled by remember(context) {
mutableStateOf(SettingsActivity.isBackgroundServiceEnabled(context))
var backgroundServiceEnabled by remember { mutableStateOf<Boolean?>(null) }
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(context) {
val repository = AppSettingsRepository(context)
backgroundServiceEnabled = repository.getSettings().backgroundServiceEnabled
}
var notificationEnabled by remember(context, notificationService) {
mutableStateOf(notificationService.isNotificationEnabled())
}
@@ -225,19 +226,25 @@ fun SettingsScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = backgroundServiceEnabled,
onCheckedChange = { enabled ->
backgroundServiceEnabled = enabled
SettingsActivity.setBackgroundServiceEnabled(context, enabled)
if (enabled) {
BackgroundService.startService(context)
} else {
BackgroundService.stopService(context)
if (backgroundServiceEnabled == null) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Switch(
checked = backgroundServiceEnabled!!,
onCheckedChange = { enabled ->
backgroundServiceEnabled = enabled
coroutineScope.launch {
val repository = AppSettingsRepository(context)
repository.updateBackgroundServiceEnabled(enabled)
if (enabled) {
BackgroundService.startService(context)
} else {
BackgroundService.stopService(context)
}
}
}
}
)
)
}
}
Row(
@@ -421,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 = "LBJ Console v$appVersion by undef-i",
style = MaterialTheme.typography.bodySmall,
@@ -435,4 +502,4 @@ fun SettingsScreen(
.padding(12.dp)
)
}
}
}

View File

@@ -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")
}
}

View File

@@ -4,8 +4,8 @@ import android.util.Log
import org.osmdroid.util.GeoPoint
object LocationUtils {
private const val TAG = "LocationUtils"
object LocationUtil {
private const val TAG = "LocationUtil"
fun parsePositionInfo(positionInfo: String): GeoPoint? {
@@ -52,7 +52,7 @@ object LocationUtils {
val minuteEndIndex = dmsString.indexOf('')
if (minuteEndIndex == -1) {
return degrees
return null
}
val minutes = dmsString.substring(degreeIndex + 1, minuteEndIndex).toDouble()

View File

@@ -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)
}
}

View File

@@ -0,0 +1,66 @@
package org.noxylva.lbjconsole.util
import android.content.Context
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.regex.Pattern
class TrainTypeUtil(private val context: Context) {
private val trainTypePatterns = mutableListOf<Pair<Pattern, String>>()
init {
loadTrainTypePatterns()
}
private fun loadTrainTypePatterns() {
try {
val inputStream = context.assets.open("train_number_info.csv")
val reader = BufferedReader(InputStreamReader(inputStream))
reader.useLines { lines ->
lines.forEach { line ->
if (line.isNotBlank()) {
val firstQuoteEnd = line.indexOf('"', 1)
if (firstQuoteEnd > 0 && firstQuoteEnd < line.length - 1) {
val regex = line.substring(1, firstQuoteEnd)
val remainingPart = line.substring(firstQuoteEnd + 1).trim()
if (remainingPart.startsWith(",\"") && remainingPart.endsWith("\"")) {
val type = remainingPart.substring(2, remainingPart.length - 1)
try {
val pattern = Pattern.compile(regex)
trainTypePatterns.add(Pair(pattern, type))
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun getTrainType(locoType: String, train: String): String? {
if (train.isEmpty()) {
return null
}
val actualTrain = if (locoType == "NA") {
train
} else {
locoType + train
}
for ((pattern, type) in trainTypePatterns) {
if (pattern.matcher(actualTrain).matches()) {
return type
}
}
return null
}
}

0
app/src/main/res/drawable/ic_launcher_background.xml Normal file → Executable file
View File

0
app/src/main/res/drawable/ic_launcher_foreground.xml Normal file → Executable file
View File

0
app/src/main/res/drawable/ic_notification.xml Normal file → Executable file
View File

View File

@@ -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>

View File

@@ -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>

0
app/src/main/res/layout/notification_train_record.xml Normal file → Executable file
View File

0
app/src/main/res/mipmap-anydpi/ic_launcher.xml Normal file → Executable file
View File

0
app/src/main/res/mipmap-anydpi/ic_launcher_round.xml Normal file → Executable file
View File

0
app/src/main/res/mipmap-hdpi/ic_launcher.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

0
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

0
app/src/main/res/mipmap-mdpi/ic_launcher.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 982 B

0
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

0
app/src/main/res/mipmap-xhdpi/ic_launcher.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

0
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

0
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

0
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

0
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

0
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -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,中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段,,
1 6G 51 90 西安铁路局 宝鸡电力机务段
2 6K 1 85 中国铁路郑州局集团有限公司 洛阳机务段
3 8G 1 1 中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段
4 8G 2 2 中国铁道博物馆
5 8G 3 75 中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段
6 8G 76 76 中国铁路太原局集团有限公司 太原机务段北场
7 8G 77 96 中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段
8 8G 97 97 中国铁路太原局集团有限公司 榆次机务折返段
9 8G 98 100 中国铁路太原局集团有限公司 太原北机务段、侯马机务段、石家庄电力机务段
10 8K 1 1 中国铁路北京局集团有限公司 丰台机务段
11 8K 2 7 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
12 8K 8 8 中国铁道博物馆 *科技号
13 8K 9 17 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
14 8K 18 18 *北京铁路局 丰台机务段
15 8K 19 23 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
16 8K 24 24 中国铁路太原局集团有限公司 湖东机务段 大同西运用车间
17 8K 25 64 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
18 8K 65 65 天津铁道职业技术学院
19 8K 66 71 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
20 8K 72 72 中国铁路北京局集团有限公司 丰台机务段
21 8K 73 90 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
22 8K 91 91 中国铁路太原局集团有限公司 太原机务段北场 机车展场
23 8K 92 100 *北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段
24 CR400AF 21 21 中国铁路北京局集团有限公司 朝阳动车运用所 CR400AF-G
25 CR400AF 207 208 中国铁路北京局集团有限公司 北京西动车运用所
26 CR400AF 1001 1002 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
27 CR400AF 1003 1003 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
28 CR400AF 1004 1004 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
29 CR400AF 1005 1005 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
30 CR400AF 1006 1006 中国铁路广州局集团有限公司 广州南动车运用所
31 CR400AF 1007 1009 中国铁路广州局集团有限公司 潮州动车运用所
32 CR400AF 1010 1010 中国铁路广州局集团有限公司 广州南动车运用所
33 CR400AF 1011 1014 中国铁路广州局集团有限公司 潮州动车运用所
34 CR400AF 1015 1020 中国铁路广州局集团有限公司 广州南动车运用所
35 CR400AF 1021 1021 中国铁路广州局集团有限公司 潮州动车运用所
36 CR400AF 1022 1025 中国铁路广州局集团有限公司 广州南动车运用所
37 CR400AF 1026 1027 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
38 CR400AF 1028 1029 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
39 CR400AF 1030 1030 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
40 CR400AF 1031 1031 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
41 CR400AF 1032 1032 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
42 CR400AF 1033 1033 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
43 CR400AF 1034 1038 中国铁路广州局集团有限公司 广州南动车运用所 CR400AF-A
44 CR400AF 1039 1039 中国铁路广州局集团有限公司 潮州动车运用所
45 CR400AF 1040 1040 中国铁路广州局集团有限公司 广州南动车运用所
46 CR400AF 2002 2002 中国铁路北京局集团有限公司 北京西动车运用所
47 CR400AF 2004 2004 中国铁路北京局集团有限公司 北京西动车运用所
48 CR400AF 2005 2005 中国铁路成都局集团有限公司 重庆西动车运用所
49 CR400AF 2006 2007 中国铁路北京局集团有限公司 北京西动车运用所
50 CR400AF 2008 2008 中国铁路北京局集团有限公司 雄安动车运用所
51 CR400AF 2009 2010 中国铁路北京局集团有限公司 北京西动车运用所
52 CR400AF 2011 2011 中国铁路广州局集团有限公司 长沙动车运用所
53 CR400AF 2012 2012 中国铁路北京局集团有限公司 北京西动车运用所
54 CR400AF 2013 2013 中国铁路成都局集团有限公司 重庆西动车运用所
55 CR400AF 2014 2016 中国铁路北京局集团有限公司 北京西动车运用所
56 CR400AF 2017 2017 中国铁路广州局集团有限公司 长沙动车运用所
57 CR400AF 2023 2023 中国铁路北京局集团有限公司 北京西动车运用所
58 CR400AF 2024 2024 中国铁路广州局集团有限公司 长沙动车运用所
59 CR400AF 2025 2025 中国铁路成都局集团有限公司 重庆西动车运用所
60 CR400AF 2026 2028 中国铁路广州局集团有限公司 长沙动车运用所
61 CR400AF 2030 2030 中国铁路北京局集团有限公司 雄安动车运用所
62 CR400AF 2031 2032 中国铁路成都局集团有限公司 重庆西动车运用所
63 CR400AF 2033 2033 中国铁路北京局集团有限公司 雄安动车运用所
64 CR400AF 2034 2034 中国铁路北京局集团有限公司 北京西动车运用所
65 CR400AF 2035 2038 中国铁路广州局集团有限公司 长沙动车运用所
66 CR400AF 2040 2046 中国铁路广州局集团有限公司 长沙动车运用所
67 CR400AF 2047 2048 中国铁路北京局集团有限公司 北京西动车运用所
68 CR400AF 2049 2049 中国铁路成都局集团有限公司 重庆西动车运用所
69 CR400AF 2051 2051 中国铁路广州局集团有限公司 长沙动车运用所
70 CR400AF 2053 2055 中国铁路广州局集团有限公司 长沙动车运用所
71 CR400AF 2057 2057 中国铁路广州局集团有限公司 长沙动车运用所
72 CR400AF 2058 2058 中国铁路北京局集团有限公司 雄安动车运用所
73 CR400AF 2060 2062 中国铁路广州局集团有限公司 长沙动车运用所
74 CR400AF 2064 2064 中国铁路广州局集团有限公司 长沙动车运用所
75 CR400AF 2065 2066 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
76 CR400AF 2067 2068 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
77 CR400AF 2069 2069 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
78 CR400AF 2070 2070 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
79 CR400AF 2071 2071 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
80 CR400AF 2072 2072 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
81 CR400AF 2073 2073 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
82 CR400AF 2074 2076 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
83 CR400AF 2077 2079 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
84 CR400AF 2080 2084 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
85 CR400AF 2085 2085 中国铁路济南局集团有限公司 济南东动车运用所
86 CR400AF 2086 2086 中国铁路济南局集团有限公司 青岛动车运用所
87 CR400AF 2087 2087 中国铁路济南局集团有限公司 济南东动车运用所
88 CR400AF 2088 2090 中国铁路济南局集团有限公司 青岛动车运用所
89 CR400AF 2091 2094 中国铁路济南局集团有限公司 济南东动车运用所
90 CR400AF 2095 2097 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
91 CR400AF 2098 2098 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
92 CR400AF 2099 2100 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
93 CR400AF 2102 2102 中国铁路济南局集团有限公司 济南东动车运用所 CR400AF-A
94 CR400AF 2103 2104 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
95 CR400AF 2105 2105 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
96 CR400AF 2106 2106 中国铁路广州局集团有限公司 长沙动车运用所 CR400AF-A
97 CR400AF 2107 2115 中国铁路济南局集团有限公司 济南东动车运用所 CR400AF-A
98 CR400AF 2116 2123 中国铁路北京局集团有限公司 北京南动车运用所 CR400AF-B
99 CR400AF 2124 2124 中国铁路武汉局集团有限公司 武汉动车运用所
100 CR400AF 2125 2125 中国铁路武汉局集团有限公司 武汉动车运用所
101 CR400AF 2126 2127 中国铁路武汉局集团有限公司 汉口动车运用所
102 CR400AF 2128 2128 中国铁路武汉局集团有限公司 武汉动车运用所
103 CR400AF 2130 2131 中国铁路广州局集团有限公司 长沙动车运用所
104 CR400AF 2133 2133 中国铁路广州局集团有限公司 长沙动车运用所
105 CR400AF 2134 2134 中国铁路济南局集团有限公司 青岛动车运用所
106 CR400AF 2135 2135 中国铁路成都局集团有限公司 重庆西动车运用所
107 CR400AF 2136 2138 中国铁路济南局集团有限公司 青岛动车运用所
108 CR400AF 2139 2139 中国铁路济南局集团有限公司 济南东动车运用所
109 CR400AF 2140 2140 中国铁路成都局集团有限公司 重庆西动车运用所
110 CR400AF 2141 2141 中国铁路济南局集团有限公司 济南东动车运用所
111 CR400AF 2142 2144 中国铁路北京局集团有限公司 北京西动车运用所
112 CR400AF 2145 2146 中国铁路北京局集团有限公司 雄安动车运用所
113 CR400AF 2148 2150 中国铁路武汉局集团有限公司 汉口动车运用所
114 CR400AF 2151 2151 中国铁路武汉局集团有限公司 武汉动车运用所
115 CR400AF 2152 2153 中国铁路武汉局集团有限公司 汉口动车运用所
116 CR400AF 2154 2156 中国铁路武汉局集团有限公司 武汉动车运用所
117 CR400AF 2159 2159 中国铁路武汉局集团有限公司 武汉动车运用所
118 CR400AF 2160 2161 中国铁路武汉局集团有限公司 汉口动车运用所
119 CR400AF 2162 2163 中国铁路济南局集团有限公司 济南东动车运用所
120 CR400AF 2164 2164 中国铁路武汉局集团有限公司 武汉动车运用所
121 CR400AF 2171 2172 中国铁路武汉局集团有限公司 武汉动车运用所
122 CR400AF 2173 2173 中国铁路武汉局集团有限公司 汉口动车运用所
123 CR400AF 2174 2177 中国铁路武汉局集团有限公司 武汉动车运用所
124 CR400AF 2178 2178 中国铁路北京局集团有限公司 雄安动车运用所
125 CR400AF 2179 2179 中国铁路北京局集团有限公司 北京西动车运用所
126 CR400AF 2180 2180 中国铁路北京局集团有限公司 雄安动车运用所
127 CR400AF 2181 2182 中国铁路北京局集团有限公司 北京西动车运用所
128 CR400AF 2183 2187 中国铁路北京局集团有限公司 北京南动车运用所
129 CR400AF 2190 2192 中国铁路武汉局集团有限公司 武汉动车运用所 CR400AF-A
130 CR400AF 2193 2193 中国铁路济南局集团有限公司 济南东动车运用所 CR400AF-A
131 CR400AF 2194 2195 中国铁路广州局集团有限公司 深圳动车运用所 CR400AF-A
132 CR400AF 2196 2200 中国铁路武汉局集团有限公司 武汉动车运用所 CR400AF-A
133 CR400AF 2201 2205 中国铁路济南局集团有限公司 济南东动车运用所 CR400AF-A
134 CR400AF 2206 2210 中国铁路北京局集团有限公司 北京南动车运用所 CR400AF-B
135 CR400AF 2211 2212 中国铁路济南局集团有限公司 济南东动车运用所 CR400AF-A
136 CR400AF 2213 2213 中国铁路北京局集团有限公司 北京西动车运用所
137 CR400AF 2215 2217 中国铁路北京局集团有限公司 朝阳动车运用所 CR400AF-G
138 CR400AF 2222 2225 中国铁路上海局集团有限公司 上海南动车运用所
139 CR400AF 2226 2226 中国铁路广州局集团有限公司 广州南动车运用所
140 CR400AF 2227 2227 中国铁路广州局集团有限公司 长沙动车运用所
141 CR400AF 2228 2228 中国铁路广州局集团有限公司 广州南动车运用所
142 CR400AF 2229 2229 中国铁路广州局集团有限公司 长沙动车运用所
143 CR400AF 2230 2231 中国铁路济南局集团有限公司 济南东动车运用所
144 CR400AF 2232 2235 中国铁路上海局集团有限公司 上海南动车运用所
145 CR400AF 2236 2236 中国铁路成都局集团有限公司 重庆西动车运用所
146 CR400AF 2237 2243 中国铁路上海局集团有限公司 上海南动车运用所
147 CR400AF 2244 2248 中国铁路成都局集团有限公司 重庆西动车运用所
148 CR400AF 2254 2256 中国铁路成都局集团有限公司 重庆西动车运用所
149 DJ1 1 1 中国铁道科学研究院 环形铁道
150 DJ1 2 2 株洲西门子牵引设备有限公司
151 DJ1 3 3 西安铁路局 宝鸡机务段 秦岭附加队
152 DJ2 1 1 中国铁路郑州局集团有限公司 郑州机务段京武快车队 奥星
153 DJ2 2 3 中国铁路郑州局集团有限公司 郑州机务段 奥星
154 HXD1D 1 15 中国铁路武汉局集团有限公司 武昌南机务段
155 HXD1D 16 16 中国铁路上海局集团有限公司 杭州机务段
156 HXD1D 17 17 中国铁路南昌局集团有限公司 南昌机务段
157 HXD1D 18 18 中国铁路南昌局集团有限公司 鹰潭机务段
158 HXD1D 19 19 中国铁路上海局集团有限公司 杭州机务段
159 HXD1D 20 20 中国铁路南昌局集团有限公司 南昌机务段
160 HXD1D 21 21 中国铁路上海局集团有限公司 杭州机务段
161 HXD1D 22 24 中国铁路南昌局集团有限公司 南昌机务段
162 HXD1D 25 25 中国铁路南昌局集团有限公司 鹰潭机务段
163 HXD1D 26 26 中国铁路南昌局集团有限公司 南昌机务段
164 HXD1D 27 27 中国铁路上海局集团有限公司 杭州机务段
165 HXD1D 28 28 中国铁路南昌局集团有限公司 鹰潭机务段
166 HXD1D 29 34 中国铁路南昌局集团有限公司 南昌机务段
167 HXD1D 35 35 中国铁路上海局集团有限公司 杭州机务段
168 HXD1D 36 38 中国铁路兰州局集团有限公司 兰州西机务段
169 HXD1D 39 39 中国铁路济南局集团有限公司 济南机务段
170 HXD1D 40 50 中国铁路兰州局集团有限公司 兰州西机务段
171 HXD1D 51 75 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
172 HXD1D 76 105 中国铁路兰州局集团有限公司 兰州西机务段
173 HXD1D 106 137 中国铁路上海局集团有限公司 上海机务段
174 HXD1D 138 168 中国铁路上海局集团有限公司 杭州机务段
175 HXD1D 169 175 中国铁路上海局集团有限公司 上海机务段
176 HXD1D 176 185 中国铁路武汉局集团有限公司 武昌南机务段
177 HXD1D 186 187 中国铁路南昌局集团有限公司 鹰潭机务段
178 HXD1D 188 188 中国铁路南昌局集团有限公司 南昌机务段
179 HXD1D 189 190 中国铁路南昌局集团有限公司 鹰潭机务段
180 HXD1D 191 232 中国铁路南昌局集团有限公司 南昌机务段
181 HXD1D 233 233 中国铁路南昌局集团有限公司 鹰潭机务段
182 HXD1D 234 237 中国铁路南昌局集团有限公司 南昌机务段
183 HXD1D 238 257 中国铁路广州局集团有限公司 广州机务段
184 HXD1D 258 270 中国铁路武汉局集团有限公司 武昌南机务段
185 HXD1D 271 275 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
186 HXD1D 276 279 中国铁路上海局集团有限公司 上海机务段
187 HXD1D 280 289 中国铁路上海局集团有限公司 徐州机务段
188 HXD1D 290 291 中国铁路南昌局集团有限公司 南昌机务段
189 HXD1D 292 293 中国铁路南昌局集团有限公司 鹰潭机务段
190 HXD1D 294 295 中国铁路南昌局集团有限公司 南昌机务段
191 HXD1D 296 300 中国铁路广州局集团有限公司 广州机务段
192 HXD1D 301 310 中国铁路武汉局集团有限公司 武昌南机务段
193 HXD1D 311 320 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
194 HXD1D 321 340 中国铁路青藏集团有限公司 西宁机务段
195 HXD1D 341 362 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
196 HXD1D 363 382 中国铁路广州局集团有限公司 广州机务段
197 HXD1D 383 392 中国铁路南昌局集团有限公司 南昌机务段
198 HXD1D 393 405 中国铁路上海局集团有限公司 上海机务段
199 HXD1D 406 415 中国铁路兰州局集团有限公司 兰州西机务段
200 HXD1D 416 430 中国铁路广州局集团有限公司 长沙机务段
201 HXD1D 431 440 中国铁路南昌局集团有限公司 南昌机务段
202 HXD1D 441 445 中国铁路南昌局集团有限公司 南昌机务段
203 HXD1D 446 450 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
204 HXD1D 451 460 中国铁路武汉局集团有限公司 武昌南机务段
205 HXD1D 461 470 中国铁路广州局集团有限公司 广州机务段
206 HXD1D 471 478 中国铁路兰州局集团有限公司 兰州西机务段
207 HXD1D 479 483 中国铁路上海局集团有限公司 上海机务段
208 HXD1D 484 488 中国铁路上海局集团有限公司 杭州机务段
209 HXD1D 489 490 中国铁路上海局集团有限公司 杭州机务段
210 HXD1D 491 510 中国铁路郑州局集团有限公司 郑州机务段
211 HXD1D 511 512 中国铁路上海局集团有限公司 徐州机务段
212 HXD1D 513 515 中国铁路上海局集团有限公司 上海机务段
213 HXD1D 516 520 中国铁路南昌局集团有限公司 南昌机务段
214 HXD1D 521 534 中国铁路广州局集团有限公司 广州机务段
215 HXD1D 522 522 广州铁路职业技术学院
216 HXD1D 535 544 中国铁路南昌局集团有限公司 南昌机务段
217 HXD1D 545 551 中国铁路上海局集团有限公司 上海机务段
218 HXD1D 552 554 中国铁路上海局集团有限公司 杭州机务段
219 HXD1D 555 559 中国铁路郑州局集团有限公司 郑州机务段
220 HXD1D 560 564 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
221 HXD1D 565 570 中国铁路广州局集团有限公司 广州机务段
222 HXD1D 571 585 中国铁路南昌局集团有限公司 南昌机务段
223 HXD1D 586 595 中国铁路上海局集团有限公司 杭州机务段
224 HXD1D 596 613 中国铁路兰州局集团有限公司 兰州西机务段
225 HXD1D 614 623 中国铁路广州局集团有限公司 广州机务段
226 HXD1D 624 633 中国铁路上海局集团有限公司 上海机务段
227 HXD1D 634 636 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
228 HXD1D 637 644 中国铁路郑州局集团有限公司 郑州机务段
229 HXD1D 645 660 中国铁路郑州局集团有限公司 郑州机务段
230 HXD1D 661 668 中国铁路武汉局集团有限公司 武昌南机务段
231 HXD1D 669 673 中国铁路乌鲁木齐局集团有限公司 乌鲁木齐机务段
232 HXD1D 674 678 中国铁路郑州局集团有限公司 郑州机务段
233 HXD1D 679 682 中国铁路上海局集团有限公司 上海机务段
234 HXD1D 683 683 中国铁路上海局集团有限公司 徐州机务段
235 HXD1D 684 684 中国铁路上海局集团有限公司 徐州机务段
236 HXD1D 685 689 中国铁路青藏集团有限公司 格尔木机务段
237 HXD1D 1898 1898 中国铁路上海局集团有限公司 上海机务段 周恩来号
238 HXD1D-J 1 3 中国铁路青藏集团有限公司 拉萨动车运用所
239 HXD1D-J 1001 1009 中国铁路昆明局集团有限公司 昆明动车运用所
240 HXD1D-J 1010 1013 中国铁路青藏集团有限公司 格尔木机务段
241 HXD1D-J 1014 1019 中国铁路成都局集团有限公司 成都动车运用所
242 HXD1D-J 1020 1027 中国铁路昆明局集团有限公司 昆明动车运用所
243 HXD3C 1 9 中国铁路沈阳局集团有限公司 沈阳机务段
244 HXD3C 10 10 中国铁路沈阳局集团有限公司 沈阳机务段
245 HXD3C 11 15 中国铁路济南局集团有限公司 济南机务段
246 HXD3C 16 20 中国铁路武汉局集团有限公司 江岸机务段(襄阳机务段支配)
247 HXD3C 21 25 中国铁路南昌局集团有限公司 南昌机务段
248 HXD3C 26 30 中国铁路郑州局集团有限公司 郑州机务段
249 HXD3C 31 35 中国铁路上海局集团有限公司 宁东机务段(上海机务段支配)
250 HXD3C 36 41 中国铁路武汉局集团有限公司 江岸机务段(武南机务段支配)
251 HXD3C 42 45 中国铁路武汉局集团有限公司 江岸机务段(襄阳机务段支配)
252 HXD3C 46 55 中国铁路南昌局集团有限公司 南昌机务段
253 HXD3C 56 60 中国铁路郑州局集团有限公司 郑州机务段
254 HXD3C 61 61 中国铁路沈阳局集团有限公司 沈阳机务段
255 HXD3C 62 62 中国铁路济南局集团有限公司 济南机务段
256 HXD3C 63 63 中国铁路沈阳局集团有限公司 沈阳机务段
257 HXD3C 64 70 中国铁路济南局集团有限公司 济南机务段
258 HXD3C 71 85 中国铁路武汉局集团有限公司 江岸机务段
259 HXD3C 86 95 中国铁路郑州局集团有限公司 郑州机务段
260 HXD3C 96 100 中国铁路济南局集团有限公司 济南机务段
261 HXD3C 101 110 中国铁路武汉局集团有限公司 江岸机务段
262 HXD3C 111 120 中国铁路沈阳局集团有限公司 沈阳机务段
263 HXD3C 121 125 中国铁路上海局集团有限公司 宁东机务段(上海机务段支配)
264 HXD3C 126 130 中国铁路南昌局集团有限公司 南昌机务段
265 HXD3C 131 135 中国铁路济南局集团有限公司 济南机务段
266 HXD3C 136 140 中国铁路郑州局集团有限公司 郑州机务段
267 HXD3C 141 165 中国铁路武汉局集团有限公司 江岸机务段
268 HXD3C 166 180 中国铁路成都局集团有限公司 重庆机务段
269 HXD3C 181 182 中国铁路济南局集团有限公司 济南机务段
270 HXD3C 183 190 中国铁路沈阳局集团有限公司 沈阳机务段
271 HXD3C 191 195 中国铁路济南局集团有限公司 济南机务段
272 HXD3C 198 200 中国铁路上海局集团有限公司 宁东机务段
273 HXD3C 201 220 中国铁路武汉局集团有限公司 江岸机务段
274 HXD3C 221 225 中国铁路上海局集团有限公司 宁东机务段
275 HXD3C 226 229 中国铁路沈阳局集团有限公司 沈阳机务段
276 HXD3C 238 238 中国铁路广州局集团有限公司 株洲机务段
277 HXD3C 271 300 中国铁路上海局集团有限公司 宁东机务段
278 HXD3C 446 446 中国铁路广州局集团有限公司 广州机务段
279 HXD3C 805 809 中国铁路广州局集团有限公司
280 HXD3C 810 819 中国铁路南宁局集团有限公司
281 HXD3C 820 829 中国铁路武汉局集团有限公司
282 HXD3C 896 925 中国铁路沈阳局集团有限公司
283 HXD3C 926 930 中国铁路南宁局集团有限公司
284 HXD3C 931 945 中国铁路北京局集团有限公司
285 HXD3C 946 955 中国铁路济南局集团有限公司
286 HXD3C 956 965 中国铁路郑州局集团有限公司
287 HXD3C 966 974 中国铁路济南局集团有限公司
288 HXD3D 1 10 中国铁路沈阳局集团有限公司 沈阳机务段
289 HXD3D 11 25 西安铁路局集团有限公司 西安机务段
290 HXD3D 26 34 中国铁路兰州局集团有限公司 兰州西机务段
291 HXD3D 35 35 中国铁路兰州局集团有限公司 迎水桥机务段 雷锋号
292 HXD3D 36 38 中国铁路兰州局集团有限公司 兰州西机务段
293 HXD3D 39 39 中国铁路济南局集团有限公司 济南机务段 共青团号
294 HXD3D 40 40 中国铁路兰州局集团有限公司 兰州西机务段
295 HXD3D 41 50 西安铁路局集团有限公司 西安机务段
296 HXD3D 51 70 中国铁路北京局集团有限公司 北京机务段
297 HXD3D 71 90 中国铁路南昌局集团有限公司 南昌机务段
298 HXD3D 91 115 中国铁路兰州局集团有限公司 兰州西机务段
299 HXD3D 116 135 中国铁路昆明局集团有限公司 昆明机务段
300 HXD3D 136 145 中国铁路北京局集团有限公司 北京机务段
301 HXD3D 146 150 呼和浩特铁路局集团有限公司 集宁机务段
302 HXD3D 151 155 中国铁路沈阳局集团有限公司 沈阳机务段
303 HXD3D 156 160 中国铁路南昌局集团有限公司 南昌机务段
304 HXD3D 161 165 中国铁路北京局集团有限公司 北京机务段
305 HXD3D 166 170 西安铁路局集团有限公司 西安机务段
306 HXD3D 171 180 中国铁路兰州局集团有限公司 兰州西机务段
307 HXD3D 181 190 中国铁路济南局集团有限公司 济南机务段
308 HXD3D 191 245 中国铁路沈阳局集团有限公司 沈阳机务段
309 HXD3D 246 255 中国铁路北京局集团有限公司 北京机务段
310 HXD3D 256 265 呼和浩特铁路局集团有限公司 集宁机务段
311 HXD3D 266 290 中国铁路兰州局集团有限公司 兰州西机务段
312 HXD3D 291 300 中国铁路沈阳局集团有限公司 沈阳机务段
313 HXD3D 301 310 中国铁路济南局集团有限公司 济南机务段
314 HXD3D 310 315 中国铁路昆明局集团有限公司 昆明机务段
315 HXD3D 316 320 中国铁路南昌局集团有限公司 南昌机务段
316 HXD3D 321 322 呼和浩特铁路局集团有限公司 集宁机务段
317 HXD3D 323 325 中国铁路北京局集团有限公司 北京机务段
318 HXD3D 326 333 西安铁路局集团有限公司 西安机务段
319 HXD3D 334 340 西安铁路局集团有限公司 安康机务段
320 HXD3D 341 345 中国铁路沈阳局集团有限公司 沈阳机务段
321 HXD3D 346 346 中国铁路成都局集团有限公司 重庆机务段
322 HXD3D 351 351 中国铁路成都局集团有限公司 重庆机务段
323 HXD3D 356 365 西安铁路局集团有限公司 安康机务段
324 HXD3D 366 369 中国铁路北京局集团有限公司 北京机务段
325 HXD3D 370 382 呼和浩特铁路局集团有限公司 集宁机务段
326 HXD3D 383 392 中国铁路昆明局集团有限公司 昆明机务段
327 HXD3D 393 397 中国铁路兰州局集团有限公司 兰州西机务段
328 HXD3D 398 402 西安铁路局集团有限公司 西安机务段
329 HXD3D 403 417 西安铁路局集团有限公司 西安机务段
330 HXD3D 418 419 中国铁路哈尔滨局集团有限公司 牡丹江机务段
331 HXD3D 420 424 中国铁路北京局集团有限公司 北京机务段
332 HXD3D 425 429 呼和浩特铁路局集团有限公司 集宁机务段
333 HXD3D 430 433 中国铁路哈尔滨局集团有限公司 牡丹江机务段
334 HXD3D 434 443 中国铁路南昌局集团有限公司 南昌机务段
335 HXD3D 444 449 中国铁路济南局集团有限公司 济南机务段
336 HXD3D 450 464 西安铁路局集团有限公司 西安机务段
337 HXD3D 465 468 中国铁路济南局集团有限公司 济南机务段
338 HXD3D 469 473 中国铁路济南局集团有限公司 济南机务段
339 HXD3D 474 479 中国铁路昆明局集团有限公司 昆明机务段
340 HXD3D 480 484 中国铁路南昌局集团有限公司 南昌机务段
341 HXD3D 485 489 西安铁路局集团有限公司 西安机务段
342 HXD3D 490 499 中国铁路北京局集团有限公司 北京机务段
343 HXD3D 500 503 中国铁路哈尔滨局集团有限公司 牡丹江机务段
344 HXD3D 504 514 中国铁路沈阳局集团有限公司 沈阳机务段
345 HXD3D 515 515 中国铁路成都局集团有限公司 重庆机务段
346 HXD3D 516 518 中国铁路沈阳局集团有限公司 沈阳机务段
347 HXD3D 519 528 西安铁路局集团有限公司 西安机务段
348 HXD3D 529 538 中国铁路北京局集团有限公司 北京机务段
349 HXD3D 539 541 呼和浩特铁路局集团有限公司 集宁机务段
350 HXD3D 542 553 西安铁路局集团有限公司 西安机务段
351 HXD3D 554 563 中国铁路济南局集团有限公司 济南机务段
352 HXD3D 564 568 中国铁路昆明局集团有限公司 昆明机务段
353 HXD3D 569 573 中国铁路成都局集团有限公司 重庆机务段
354 HXD3D 574 583 中国铁路济南局集团有限公司 济南机务段
355 HXD3D 584 584 中国铁路沈阳局集团有限公司 沈阳机务段
356 HXD3D 585 609 中国铁路哈尔滨局集团有限公司 三棵树机务段
357 HXD3D 610 611 中国铁路北京局集团有限公司 北京机务段
358 HXD3D 612 621 中国铁路南昌局集团有限公司 南昌机务段
359 HXD3D 622 626 中国铁路南昌局集团有限公司 南昌机务段
360 HXD3D 627 629 中国铁路哈尔滨局集团有限公司 三棵树机务段
361 HXD3D 630 630 中国铁路哈尔滨局集团有限公司 哈尔滨机务段
362 HXD3D 631 631 西安铁路局集团有限公司 西安机务段 第五代“钢人铁马号”
363 HXD3D 632 653 中国铁路沈阳局集团有限公司 沈阳机务段
364 HXD3D 654 673 中国铁路沈阳局集团有限公司 沈阳机务段
365 HXD3D 674 681 中国铁路沈阳局集团有限公司 沈阳机务段
366 HXD3D 682 688 中国铁路兰州局集团有限公司 兰州西机务段
367 HXD3D 1886 1886 中国铁路哈尔滨局集团有限公司 哈尔滨机务段 第五代“朱德号”
368 HXD3D 1893 1893 中国铁路北京局集团有限公司 丰台机务段 第六代“毛泽东号”
369 HXD3D 1921 1921 中国铁路沈阳局集团有限公司 沈阳机务段 共产党员号
370 HXD3D 7001 7002 广西沿海铁路股份有限公司 南宁南机务运用段
371 HXD3D 7003 7003 吉林铁道职业技术学院
372 HXD3D 8001 8025 中国铁路沈阳局集团有限公司 沈阳机务段 大同
373 HXD3D 8026 8028 中国铁路太原局集团有限公司 太原南机务段 大同
374 东方红2 1 50 资阳
375 东风 1201 1830 大连、成都
376 东风 2001 2094 戚墅堰
377 东风11 1 459 戚墅堰
378 东风12 8001 8001 吉林铁道职业技术学院
379 东风2 3201 3348 戚墅堰
380 东风21 1 5 中国铁路昆明局集团有限公司 昆明机务段
381 东风21 6 6 中国铁路昆明局集团有限公司 昆明机务段 状元号
382 东风21 7 7 中国铁路昆明局集团有限公司 昆明机务段 亲年号
383 东风21 8 8 中国铁路昆明局集团有限公司 昆明机务段 建水古城
384 东风21 9 100 中国铁路昆明局集团有限公司 昆明机务段
385 东风21 101 101 中国铁路昆明局集团有限公司 昆明机务段 异龙号
386 东风21 102 102 中国铁路昆明局集团有限公司 昆明机务段
387 东风21 1001 1002 云南钢铁厂
388 东风2Z 3251 3251 *齐齐哈尔铁路局 加格达奇机务段
389 东风3 3243 3243 中车共享城机车公园
390 东风4 3247 3247 中车成都轨道交通产业园
391 东风4B 1001 1999 大连
392 东风4B 1963 1963 *北京铁路局 丰台机务段
393 东风4B 2101 2685 大连
394 东风4B 2104 2104 *上海铁路局 蚌埠机务段
395 东风4B 2376 2376 *南昌铁路局 鹰潭机务段
396 东风4B 3101 3999 资阳
397 东风4B 3214 3214 *浙江金温铁道开发有限公司 温州机务段
398 东风4B 3249 3249 *西安铁路局 西安机务段
399 东风4B 3390 3390 *成都铁路局 重庆机务段
400 东风4B 3593 3593 *中国铁路广州局集团有限公司 株洲机务段
401 东风4B 6001 6587 大同
402 东风4B 6530 6530 *南宁铁路局 南宁机务段
403 东风4B 7001 7363 大连
404 东风4B 7364 7365 四方
405 东风4B 7366 7796 大连
406 东风4B 7701 7732 戚墅堰改
407 东风4B 9001 9702 资阳
408 东风4B 9167 9167 *南昌铁路局 向塘机务段
409 东风4B 9531 9531 *新长铁路公司
410 东风4C 1 10 大同
411 东风4C 11 11 中国铁路北京局集团有限公司 丰台段 青年文明号
412 东风4C 12 40 大同
413 东风4C 2001 2006 四方
414 东风4C 4001 4465 大连
415 东风4C 4466 4466 四方机车车辆厂 四方
416 东风4C 5001 5273 资阳
417 东风4C 5274 5275 三茂铁路公司 三水机务段 东风4CK
418 东风4C 5276 5335 资阳
419 东风4D 7001 7021 中国铁路南宁局集团有限公司
420 东风5 1 1 中国铁路北京局集团有限公司 北京车辆段
421 东风5 1974 1975 中国铁路兰州局集团有限公司 兰州西机务段 唐山
422 东风5 1976 2082 唐山
423 东风5 2083 2083 中国石油兰州石化公司 唐山
424 东风5 3279 3279 云南铁路博物馆
425 东风6 1 2 *沈阳铁路局 大连机务段
426 东风6 3 3 沈阳铁路陈列馆
427 东风6 4 4 *沈阳铁路局 大连机务段
428 东风7 174 174 中国铁路太原局集团有限公司 太原机务段北场
429 东风7B 3006 3006 中国铁道博物馆
430 东风7B 3015 3015 王坪村铁路公园
431 东风7B 6001 6072 *北京铁路局 邯郸机务段;郑州铁路局 新乡机务段 调车
432 东风7D 1 1 中国铁道博物馆
433 东风7D 3001 3001 中国铁道博物馆
434 东风7E 1 1 中国铁路郑州局集团有限公司 新乡机务段
435 东风7E 2 2 中国铁路郑州局集团有限公司 郑州机务段
436 东风7G 9001 9004 呼和浩特铁路局 集宁机务段 赛汗塔拉分段
437 东风8 1 1 中国铁道博物馆
438 东风9 1 2 中国铁路广州局集团有限公司广州机务段
439 韶山1 8 8 中国铁道博物馆
440 韶山1 156 156 郑州世纪欢乐园
441 韶山1 160 160 北京铁路电气化学校
442 韶山1 227 227 中国铁路兰州局集团有限公司 兰州西机务段
443 韶山1 254 254 中国铁路北京局集团有限公司 丰台机务段 储备厂
444 韶山1 307 307 中国铁路太原局集团有限公司 榆次机务折返段
445 韶山1 309 309 中国铁路太原局集团有限公司 太原机务段北场
446 韶山1 321 321 武汉铁路职业技术学院
447 韶山1 681 681 中国铁道博物馆
448 韶山1 695 695 沈阳铁路陈列馆
449 韶山1 762 762 中国铁路广州局集团有限公司 娄底运用车间储备厂
450 韶山1 818 818 西南交通大学 机车博物园
451 韶山1 821 821 韶关机务实训基地
452 韶山1 826 826 韶关机务实训基地
453 韶山3 454 454 中国铁路成都局集团有限公司 贵阳机务段 先锋号
454 韶山3 524 524 中国铁路武汉局集团有限公司 江岸机务段 青年号
455 韶山3 4160 4160 广西沿海铁路公司 南宁南机务运用段 共青团号
456 韶山3 4178 4178 广西沿海铁路公司 南宁南机务运用段 共青团号
457 韶山3 4235 4235 中国铁路成都局集团有限公司 重庆机务段 青年文明号
458 韶山3 4258 4258 中国铁路成都局集团有限公司 重庆机务段 党员先锋号
459 韶山3 5080 5080 广州铁路博物馆
460 韶山3 6005 6005 湖南交通工程学院
461 韶山3 8050 8050 武汉四美塘铁路遗址公园
462 韶山3B 16 16 西安铁路局 安康机务段 青年文明号
463 韶山3B 5001 5001 中国铁路成都局集团有限公司 贵阳机务段 *先锋力神
464 韶山3B 5035 5035 中国铁路兰州局集团有限公司 迎水桥机务段 雷锋号 (曾)
465 韶山3B 5038 5038 中国铁路兰州局集团有限公司 迎水桥机务段 青年文明号
466 韶山3B 5151 5151 中国铁路成都局集团有限公司 西昌机务段 扶贫先锋号
467 韶山3B 5162 5162 中国铁路昆明局集团有限公司 昆明机务段 五四青年号
468 韶山3B 5235 5235 中国铁路成都局集团有限公司 西昌机务段 *共青团号
469 韶山3C 1 1 中国铁路成都局集团有限公司 贵阳机务段
470 韶山4 6 6 中国铁道博物馆
471 韶山4 10 10 中国铁路成都局集团有限公司 西昌机务段
472 韶山4 50 50 中国铁路郑州局集团有限公司 新乡机务段 先锋号
473 韶山4 63 63 中国铁路太原局集团有限公司 太原机务段
474 韶山4 204 204 中国铁路郑州局集团有限公司 新乡机务段 先锋号
475 韶山4 448 448 中国铁路沈阳局集团有限公司 苏家屯机务段 先锋号
476 韶山4 574 574 中铁三局集团 先锋号
477 韶山4 743 743 中国铁路哈尔滨局集团有限公司 哈尔滨机务段 青年文明号
478 韶山4 855 855 西安铁路局 新丰镇机务段
479 韶山4 911 911 中铁三局集团 青年文明号
480 韶山4 2006 2006 吉林铁道职业技术学院
481 韶山4B 19 19 神朔铁路公司 神木北机务段 青年号
482 韶山4B 89 89 神朔铁路公司 神木北机务段 青年文明号
483 韶山4B 90 90 神朔铁路公司 神木北机务段 青年文明号
484 韶山4B 257 257 包神铁路公司 东胜机务段 党员先锋号
485 韶山4G 159 1177 株洲
486 韶山4G 168 168 中国铁道博物馆
487 韶山4G 171 171 中国铁路哈尔滨局集团有限公司 牡丹江机务段
488 韶山4G 179 179 中国铁路太原局集团有限公司 湖东机务段
489 韶山4G 466 466 石家庄铁道大学
490 韶山4G 1089 1089 *呼和浩特铁路局 包头西机务段
491 韶山4G 1886 1886 中国铁路哈尔滨局集团有限公司 哈尔滨机务段 *朱德号 株洲
492 韶山4G 3001 3002 资阳
493 韶山4G 6001 6001 中国铁道博物馆
494 韶山4G 6001 6001 中国铁道博物馆 大同
495 韶山4G 7001 7110 大连
496 韶山4G 7121 7243 大连
497 韶山5 1 1 中国铁道博物馆
498 韶山5 2 2 郑州世纪欢乐园
499 韶山6 1 1 郑州铁路司机学校
500 韶山6 2 2 中国铁道博物馆
501 韶山6B 1011 1011 西安铁路局 西安机务段 *青年文明号
502 韶山6B 1026 1026 韶关机务实训基地
503 韶山6B 1088 1088 中国铁路武汉局集团有限公司 襄阳机务段 *民兵号
504 韶山6B 1111 1111 中国铁路武汉局集团有限公司 襄阳机务段 *先锋号
505 韶山6B 6001 6001 韶关机务实训基地
506 韶山6B 6002 6002 广州铁路博物馆
507 韶山7 1 79 中国铁路南宁局集团有限公司 柳州机务段
508 韶山7 76 76 中国铁路南宁局集团有限公司 南宁机务段 *五四红旗号
509 韶山7 80 84 中国铁路南宁局集团有限公司 柳州机务段
510 韶山7 85 111 中国铁路南宁局集团有限公司 柳州机务段
511 韶山7 8112 8113 山西孝柳铁路有限责任公司
512 韶山7B 1 1 *南宁铁路局集团有限公司 南宁机务段
513 韶山7B 2 2 中国铁路南宁局集团有限公司 柳州机务段
514 韶山7D 1 58 西安铁路局集团有限公司 西安机务段
515 韶山7D 631 631 西安铁路局集团有限公司 西安机务段 *钢人铁马号
516 韶山7E 1 140 大同
517 韶山7E 6001 6002 中国铁路昆明局集团有限公司 大同
518 韶山7E 7001 7004 大连
519 韶山8 1 1 中国铁路广州局集团有限公司 广州机务段
520 韶山8 2 2 中国铁路广州局集团有限公司 广州机务段
521 韶山8 3 4 中国铁路上海局集团有限公司 上海机务段
522 韶山8 5 5 中国铁路郑州局集团有限公司 郑州机务段
523 韶山8 9 9 中国铁路郑州局集团有限公司 郑州机务段
524 韶山8 11 11 中国铁路郑州局集团有限公司 郑州机务段
525 韶山8 12 12 中国铁路郑州局集团有限公司 郑州机务段
526 韶山8 15 16 中国铁路郑州局集团有限公司 郑州机务段
527 韶山8 17 17 中国铁路上海局集团有限公司 上海机务段
528 韶山8 20 20 中国铁路郑州局集团有限公司 郑州机务段
529 韶山8 24 25 中国铁路郑州局集团有限公司 郑州机务段
530 韶山8 27 27 中国铁路郑州局集团有限公司 郑州机务段
531 韶山8 29 32 中国铁路郑州局集团有限公司 郑州机务段
532 韶山8 33 35 中国铁路上海局集团有限公司 上海机务段
533 韶山8 36 36 中国铁路郑州局集团有限公司 郑州机务段
534 韶山8 38 38 中国铁路上海局集团有限公司 上海机务段
535 韶山8 39 39 中国铁路上海局集团有限公司 上海机务段 国祥号
536 韶山8 40 40 中国铁路上海局集团有限公司 上海机务段
537 韶山8 41 41 中国铁路北京局集团有限公司 北京机务段
538 韶山8 43 43 中国铁路郑州局集团有限公司 郑州机务段
539 韶山8 44 44 中国铁路北京局集团有限公司 邯郸机务段
540 韶山8 45 45 中国铁路郑州局集团有限公司 郑州机务段
541 韶山8 48 48 中国铁路北京局集团有限公司 邯郸机务段
542 韶山8 49 49 中国铁路南昌局集团有限公司 南昌机务段
543 韶山8 50 50 中国铁路南昌局集团有限公司 南昌机务段
544 韶山8 51 51 中国铁路北京局集团有限公司 邯郸机务段
545 韶山8 52 52 中国铁路上海局集团有限公司 上海机务段
546 韶山8 55 55 中国铁路南昌局集团有限公司 南昌机务段
547 韶山8 56 57 中国铁路北京局集团有限公司 邯郸机务段
548 韶山8 64 64 中国铁路广州局集团有限公司 广州机务段
549 韶山8 72 72 中国铁路北京局集团有限公司 邯郸机务段
550 韶山8 73 73 中国铁路北京局集团有限公司 北京机务段
551 韶山8 74 74 中国铁路北京局集团有限公司 邯郸机务段
552 韶山8 81 81 中国铁路北京局集团有限公司 北京机务段
553 韶山8 83 84 中国铁路郑州局集团有限公司 郑州机务段
554 韶山8 85 85 中国铁路北京局集团有限公司 北京机务段
555 韶山8 88 103 中国铁路郑州局集团有限公司 郑州机务段
556 韶山8 104 104 中国铁路北京局集团有限公司 邯郸机务段
557 韶山8 109 111 中国铁路南昌局集团有限公司 南昌机务段
558 韶山8 114 116 中国铁路南昌局集团有限公司 南昌机务段
559 韶山8 118 119 中国铁路北京局集团有限公司 北京机务段
560 韶山8 121 126 中国铁路北京局集团有限公司 北京机务段
561 韶山8 127 128 中国铁路郑州局集团有限公司 郑州机务段
562 韶山8 130 130 中国铁路南昌局集团有限公司 南昌机务段
563 韶山8 131 131 中国铁路广州局集团有限公司 长沙机务段
564 韶山8 132 132 中国铁路广州局集团有限公司 长沙机务段
565 韶山8 133 133 中国铁路广州局集团有限公司 长沙机务段
566 韶山8 134 134 中国铁路广州局集团有限公司 长沙机务段
567 韶山8 136 136 中国铁路广州局集团有限公司 长沙机务段
568 韶山8 141 141 中国铁路广州局集团有限公司 广州机务段
569 韶山8 144 144 中国铁路广州局集团有限公司 长沙机务段
570 韶山8 148 148 中国铁路广州局集团有限公司 广州机务段
571 韶山8 156 156 中国铁路广州局集团有限公司 广州机务段
572 韶山8 163 163 中国铁路广州局集团有限公司 广州机务段
573 韶山8 166 166 中国铁路广州局集团有限公司 广州机务段 新世纪金龙号
574 韶山8 171 171 中国铁路上海局集团有限公司 上海机务段
575 韶山8 172 172 中国铁路郑州局集团有限公司 郑州机务段
576 韶山8 173 173 中国铁路广州局集团有限公司 广州机务段
577 韶山8 181 181 中国铁路广州局集团有限公司 广州机务段
578 韶山8 186 186 中国铁路广州局集团有限公司 广州机务段
579 韶山8 191 191 中国铁路广州局集团有限公司 广州机务段
580 韶山8 192 192 中国铁路广州局集团有限公司 广州机务段
581 韶山8 197 197 中国铁路郑州局集团有限公司 郑州机务段
582 韶山8 200 204 中国铁路上海局集团有限公司 上海机务段
583 韶山8 205 205 中国铁路广州局集团有限公司 长沙机务段
584 韶山8 214 214 中国铁路郑州局集团有限公司 郑州机务段
585 韶山9 1 3 中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段
586 韶山9 5 29 中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段
587 韶山9 30 30 中国铁路沈阳局集团有限公司 通辽机务段
588 韶山9 31 37 中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段
589 韶山9 38 38 中国铁路沈阳局集团有限公司 通辽机务段
590 韶山9 39 43 中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段

0
app/src/main/res/values/colors.xml Normal file → Executable file
View File

0
app/src/main/res/values/strings.xml Normal file → Executable file
View File

0
app/src/main/res/values/themes.xml Normal file → Executable file
View File

0
app/src/main/res/xml/backup_rules.xml Normal file → Executable file
View File

0
app/src/main/res/xml/data_extraction_rules.xml Normal file → Executable file
View File

0
app/src/main/res/xml/file_paths.xml Normal file → Executable file
View File

View File

@@ -20,4 +20,4 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonTransitiveRClass=true

View File

@@ -8,6 +8,8 @@ espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.9.0"
activityCompose = "1.10.1"
composeBom = "2024.04.01"
room = "2.6.1"
startup = "1.1.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -24,9 +26,14 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-startup-runtime = { group = "androidx.startup", name = "startup-runtime", version.ref = "startup" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version = "2.0.0-1.0.21" }

0
gradlew vendored Normal file → Executable file
View File

View File

@@ -19,6 +19,6 @@ dependencyResolutionManagement {
}
}
rootProject.name = "LBJ Receiver"
rootProject.name = "LBJ_Console"
include(":app")