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