Initialize LBJ Receiver project with basic structure and configuration files
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
65
app/build.gradle.kts
Normal file
@@ -0,0 +1,65 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "receiver.lbj"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "receiver.lbj"
|
||||
minSdk = 29
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
implementation("org.json:json:20231013")
|
||||
implementation("androidx.compose.material:material-icons-extended:1.5.4")
|
||||
|
||||
|
||||
implementation("org.osmdroid:osmdroid-android:6.1.16")
|
||||
implementation("org.osmdroid:osmdroid-mapsforge:6.1.16")
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,20 @@
|
||||
package receiver.lbj
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("receiver.lbj", appContext.packageName)
|
||||
}
|
||||
}
|
||||
52
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.LBJReceiver"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.LBJReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
430
app/src/main/assets/loco_info.csv
Normal file
@@ -0,0 +1,430 @@
|
||||
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,*北京铁路局 丰台西段、丰台段;太原铁路局 大同西段、湖东段,,
|
||||
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,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,沈阳铁路局 沈阳机务段;上海铁路局 上海机务段,,
|
||||
|
489
app/src/main/java/receiver/lbj/BLEClient.kt
Normal file
@@ -0,0 +1,489 @@
|
||||
package receiver.lbj
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.*
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import java.nio.charset.StandardCharsets
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
class BLEClient(private val context: Context) : BluetoothGattCallback(),
|
||||
BluetoothAdapter.LeScanCallback {
|
||||
companion object {
|
||||
const val TAG = "LBJ_BT"
|
||||
const val SCAN_PERIOD = 10000L
|
||||
|
||||
val SERVICE_UUID = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb")
|
||||
val CHAR_UUID = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
const val CMD_GET_STATUS = "STATUS"
|
||||
|
||||
const val RESP_STATUS = "STATUS:"
|
||||
const val RESP_ERROR = "ERROR:"
|
||||
}
|
||||
|
||||
private var bluetoothGatt: BluetoothGatt? = null
|
||||
private var deviceAddress: String? = null
|
||||
private var isConnected = false
|
||||
private var isScanning = false
|
||||
private var statusCallback: ((String) -> Unit)? = null
|
||||
private var scanCallback: ((BluetoothDevice) -> Unit)? = null
|
||||
private var connectionStateCallback: ((Boolean) -> Unit)? = null
|
||||
private var trainInfoCallback: ((JSONObject) -> Unit)? = null
|
||||
private var handler = Handler(Looper.getMainLooper())
|
||||
private var targetDeviceName: String? = null
|
||||
|
||||
|
||||
fun setTrainInfoCallback(callback: (JSONObject) -> Unit) {
|
||||
trainInfoCallback = callback
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun scanDevices(targetDeviceName: String? = null, callback: (BluetoothDevice) -> Unit) {
|
||||
try {
|
||||
scanCallback = callback
|
||||
this.targetDeviceName = targetDeviceName
|
||||
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() ?: run {
|
||||
Log.e(TAG, "Bluetooth adapter unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
if (!bluetoothAdapter.isEnabled) {
|
||||
Log.e(TAG, "Bluetooth adapter disabled")
|
||||
return
|
||||
}
|
||||
|
||||
handler.postDelayed({
|
||||
stopScan()
|
||||
}, SCAN_PERIOD)
|
||||
|
||||
isScanning = true
|
||||
Log.d(TAG, "Starting BLE scan target=${targetDeviceName ?: "Any"}")
|
||||
bluetoothAdapter.startLeScan(this)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Scan security error: ${e.message}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "BLE scan failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun stopScan() {
|
||||
if (isScanning) {
|
||||
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
||||
bluetoothAdapter.stopLeScan(this)
|
||||
isScanning = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onLeScan(device: BluetoothDevice, rssi: Int, scanRecord: ByteArray) {
|
||||
|
||||
val deviceName = device.name
|
||||
if (targetDeviceName != null) {
|
||||
|
||||
if (deviceName == null || !deviceName.equals(targetDeviceName, ignoreCase = true)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
scanCallback?.invoke(device)
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun connect(address: String, onConnectionStateChange: ((Boolean) -> Unit)? = null): Boolean {
|
||||
if (address.isBlank()) {
|
||||
Log.e(TAG, "Connection failed empty address")
|
||||
handler.post { onConnectionStateChange?.invoke(false) }
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() ?: run {
|
||||
Log.e(TAG, "Bluetooth adapter unavailable")
|
||||
handler.post { onConnectionStateChange?.invoke(false) }
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
bluetoothGatt?.close()
|
||||
bluetoothGatt = null
|
||||
|
||||
val device = bluetoothAdapter.getRemoteDevice(address)
|
||||
|
||||
deviceAddress = address
|
||||
connectionStateCallback = onConnectionStateChange
|
||||
|
||||
|
||||
bluetoothGatt = device.connectGatt(context, false, this, BluetoothDevice.TRANSPORT_LE)
|
||||
|
||||
Log.d(TAG, "Connecting to address=$address")
|
||||
|
||||
|
||||
handler.postDelayed({
|
||||
if (!isConnected && deviceAddress == address) {
|
||||
Log.e(TAG, "Connection timeout reconnecting")
|
||||
|
||||
bluetoothGatt?.close()
|
||||
bluetoothGatt =
|
||||
device.connectGatt(context, false, this, BluetoothDevice.TRANSPORT_LE)
|
||||
}
|
||||
}, 10000)
|
||||
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Connection failed: ${e.message}")
|
||||
handler.post { onConnectionStateChange?.invoke(false) }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun isConnected(): Boolean {
|
||||
return isConnected
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun disconnect() {
|
||||
bluetoothGatt?.disconnect()
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun getStatus(callback: (String) -> Unit) {
|
||||
statusCallback = callback
|
||||
bluetoothGatt?.let { gatt ->
|
||||
val service = gatt.getService(SERVICE_UUID)
|
||||
if (service != null) {
|
||||
val characteristic = service.getCharacteristic(CHAR_UUID)
|
||||
if (characteristic != null) {
|
||||
characteristic.value = CMD_GET_STATUS.toByteArray()
|
||||
gatt.writeCharacteristic(characteristic)
|
||||
} else {
|
||||
Log.e(TAG, "Characteristic not found")
|
||||
statusCallback?.invoke("ERROR: Characteristic not found")
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Service not found")
|
||||
statusCallback?.invoke("ERROR: Service not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
||||
super.onServicesDiscovered(gatt, status)
|
||||
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
Log.i(TAG, "Discovered GATT services")
|
||||
requestMtu(gatt)
|
||||
} else {
|
||||
Log.w(TAG, "Service discovery failed status=$status")
|
||||
|
||||
handler.post {
|
||||
connectionStateCallback?.invoke(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun requestMtu(gatt: BluetoothGatt) {
|
||||
try {
|
||||
Log.d(TAG, "Requesting MTU size=512")
|
||||
gatt.requestMtu(512)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "MTU request failed: ${e.message}")
|
||||
|
||||
enableNotification()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
||||
super.onMtuChanged(gatt, mtu, status)
|
||||
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
Log.d(TAG, "MTU set to $mtu")
|
||||
} else {
|
||||
Log.w(TAG, "MTU change failed status=$status")
|
||||
}
|
||||
|
||||
|
||||
enableNotification()
|
||||
}
|
||||
|
||||
|
||||
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||
super.onConnectionStateChange(gatt, status, newState)
|
||||
|
||||
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||
Log.e(TAG, "Connection error status=$status")
|
||||
isConnected = false
|
||||
|
||||
|
||||
if (status == 133 || status == 8) {
|
||||
Log.e(TAG, "GATT error closing connection")
|
||||
try {
|
||||
gatt.close()
|
||||
bluetoothGatt = null
|
||||
|
||||
|
||||
deviceAddress?.let { address ->
|
||||
handler.postDelayed({
|
||||
Log.d(TAG, "Reconnecting to device")
|
||||
val device =
|
||||
BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address)
|
||||
bluetoothGatt = device.connectGatt(
|
||||
context,
|
||||
false,
|
||||
this,
|
||||
BluetoothDevice.TRANSPORT_LE
|
||||
)
|
||||
}, 2000)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Reconnect error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
handler.post { connectionStateCallback?.invoke(false) }
|
||||
return
|
||||
}
|
||||
|
||||
when (newState) {
|
||||
BluetoothProfile.STATE_CONNECTED -> {
|
||||
isConnected = true
|
||||
Log.i(TAG, "Connected to GATT server")
|
||||
|
||||
handler.post { connectionStateCallback?.invoke(true) }
|
||||
|
||||
|
||||
handler.postDelayed({
|
||||
try {
|
||||
gatt.discoverServices()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Service discovery failed: ${e.message}")
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
BluetoothProfile.STATE_DISCONNECTED -> {
|
||||
isConnected = false
|
||||
Log.i(TAG, "Disconnected from GATT server")
|
||||
|
||||
handler.post { connectionStateCallback?.invoke(false) }
|
||||
|
||||
|
||||
if (!deviceAddress.isNullOrBlank()) {
|
||||
handler.postDelayed({
|
||||
Log.d(TAG, "Reconnecting after disconnect")
|
||||
connect(deviceAddress!!, connectionStateCallback)
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val dataBuffer = StringBuilder()
|
||||
private val maxBufferSize = 1024 * 1024
|
||||
private var lastDataTime = 0L
|
||||
|
||||
|
||||
override fun onCharacteristicChanged(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic
|
||||
) {
|
||||
super.onCharacteristicChanged(gatt, characteristic)
|
||||
|
||||
val newData = characteristic.value?.let {
|
||||
String(it, StandardCharsets.UTF_8)
|
||||
} ?: return
|
||||
|
||||
Log.d(TAG, "Received data len=${newData.length} preview=${newData.take(50)}")
|
||||
|
||||
|
||||
dataBuffer.append(newData)
|
||||
|
||||
|
||||
checkAndProcessCompleteJson()
|
||||
}
|
||||
|
||||
|
||||
private fun checkAndProcessCompleteJson() {
|
||||
val bufferContent = dataBuffer.toString()
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
|
||||
if (lastDataTime > 0 && currentTime - lastDataTime > 5000) {
|
||||
Log.w(TAG, "Data timeout ${(currentTime - lastDataTime) / 1000}s")
|
||||
|
||||
}
|
||||
|
||||
Log.d(TAG, "Buffer size=${dataBuffer.length} bytes")
|
||||
|
||||
|
||||
tryExtractJson(bufferContent)
|
||||
|
||||
|
||||
lastDataTime = currentTime
|
||||
}
|
||||
|
||||
|
||||
private fun tryExtractJson(bufferContent: String) {
|
||||
|
||||
val openBracesCount = bufferContent.count { it == '{' }
|
||||
val closeBracesCount = bufferContent.count { it == '}' }
|
||||
|
||||
|
||||
if (openBracesCount > 0 && openBracesCount == closeBracesCount) {
|
||||
Log.d(TAG, "Found JSON braces=${openBracesCount}")
|
||||
|
||||
|
||||
val firstOpenBrace = bufferContent.indexOf('{')
|
||||
val lastCloseBrace = bufferContent.lastIndexOf('}')
|
||||
|
||||
if (firstOpenBrace >= 0 && lastCloseBrace > firstOpenBrace) {
|
||||
val possibleJson = bufferContent.substring(firstOpenBrace, lastCloseBrace + 1)
|
||||
|
||||
if (processJsonString(possibleJson)) {
|
||||
|
||||
dataBuffer.delete(0, lastCloseBrace + 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val firstOpenBrace = bufferContent.indexOf('{')
|
||||
if (firstOpenBrace >= 0) {
|
||||
|
||||
var openCount = 0
|
||||
var closeCount = 0
|
||||
var currentEnd = -1
|
||||
|
||||
for (i in firstOpenBrace until bufferContent.length) {
|
||||
if (bufferContent[i] == '{') {
|
||||
openCount++
|
||||
} else if (bufferContent[i] == '}') {
|
||||
closeCount++
|
||||
if (openCount == closeCount) {
|
||||
currentEnd = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentEnd > firstOpenBrace) {
|
||||
val possibleJson = bufferContent.substring(firstOpenBrace, currentEnd + 1)
|
||||
Log.d(TAG, "Parsing JSON=${possibleJson.take(30)}...")
|
||||
|
||||
if (processJsonString(possibleJson)) {
|
||||
|
||||
dataBuffer.delete(0, currentEnd + 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (dataBuffer.length > 1000) {
|
||||
Log.w(TAG, "Large buffer ${dataBuffer.length} bytes")
|
||||
|
||||
|
||||
val lastJsonStart = dataBuffer.lastIndexOf("{")
|
||||
if (lastJsonStart > 0) {
|
||||
dataBuffer.delete(0, lastJsonStart)
|
||||
Log.d(TAG, "Kept JSON buffer=${dataBuffer.length} bytes")
|
||||
} else {
|
||||
|
||||
dataBuffer.delete(0, dataBuffer.length / 2)
|
||||
Log.d(TAG, "Cleared buffer size=${dataBuffer.length}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun processJsonString(jsonStr: String): Boolean {
|
||||
try {
|
||||
val jsonObject = JSONObject(jsonStr)
|
||||
Log.d(TAG, "Parsed JSON len=${jsonStr.length} preview=${jsonStr.take(50)}")
|
||||
|
||||
|
||||
handler.post {
|
||||
statusCallback?.invoke(jsonStr)
|
||||
|
||||
|
||||
if (jsonObject.has("train")) {
|
||||
Log.d(TAG, "Found train data")
|
||||
trainInfoCallback?.invoke(jsonObject)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "JSON parse failed: ${e.message}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun enableNotification() {
|
||||
bluetoothGatt?.let { gatt ->
|
||||
try {
|
||||
val service = gatt.getService(SERVICE_UUID)
|
||||
if (service != null) {
|
||||
val characteristic = service.getCharacteristic(CHAR_UUID)
|
||||
if (characteristic != null) {
|
||||
val result = gatt.setCharacteristicNotification(characteristic, true)
|
||||
Log.d(TAG, "Notification set result=$result")
|
||||
|
||||
try {
|
||||
val descriptor = characteristic.getDescriptor(
|
||||
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
||||
)
|
||||
if (descriptor != null) {
|
||||
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||
val writeResult = gatt.writeDescriptor(descriptor)
|
||||
Log.d(TAG, "Descriptor write result=$writeResult")
|
||||
} else {
|
||||
Log.e(TAG, "Descriptor not found")
|
||||
|
||||
requestDataAfterDelay()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Descriptor write error: ${e.message}")
|
||||
requestDataAfterDelay()
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Characteristic not found")
|
||||
requestDataAfterDelay()
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Service not found")
|
||||
requestDataAfterDelay()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Notification setup error: ${e.message}")
|
||||
requestDataAfterDelay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun requestDataAfterDelay() {
|
||||
handler.postDelayed({
|
||||
statusCallback?.let { callback ->
|
||||
getStatus(callback)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
637
app/src/main/java/receiver/lbj/MainActivity.kt
Normal file
@@ -0,0 +1,637 @@
|
||||
package receiver.lbj
|
||||
|
||||
import android.Manifest
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import java.io.File
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.FileProvider
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
import org.osmdroid.config.Configuration
|
||||
import receiver.lbj.model.TrainRecord
|
||||
import receiver.lbj.model.TrainRecordManager
|
||||
import receiver.lbj.ui.screens.HistoryScreen
|
||||
import receiver.lbj.ui.screens.MapScreen
|
||||
import receiver.lbj.ui.screens.SettingsScreen
|
||||
import receiver.lbj.ui.screens.MapScreen
|
||||
import receiver.lbj.ui.theme.LBJReceiverTheme
|
||||
import receiver.lbj.util.LocoInfoUtil
|
||||
import java.util.*
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanResult
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val TAG = "MainActivity"
|
||||
private val bleClient by lazy { BLEClient(this) }
|
||||
private val trainRecordManager by lazy { TrainRecordManager(this) }
|
||||
private val locoInfoUtil by lazy { LocoInfoUtil(this) }
|
||||
|
||||
|
||||
private var deviceStatus by mutableStateOf("未连接")
|
||||
private var deviceAddress by mutableStateOf("")
|
||||
private var isScanning by mutableStateOf(false)
|
||||
private var foundDevices by mutableStateOf(listOf<BluetoothDevice>())
|
||||
private var scanResults = mutableListOf<ScanResult>()
|
||||
private var currentTab by mutableStateOf(0)
|
||||
private var showConnectionDialog by mutableStateOf(false)
|
||||
private var lastUpdateTime by mutableStateOf<Date?>(null)
|
||||
private var latestRecord by mutableStateOf<TrainRecord?>(null)
|
||||
private var recentRecords by mutableStateOf<List<TrainRecord>>(emptyList())
|
||||
|
||||
|
||||
private var filterTrain by mutableStateOf("")
|
||||
private var filterRoute by mutableStateOf("")
|
||||
private var filterDirection by mutableStateOf("全部")
|
||||
|
||||
|
||||
private var settingsDeviceName by mutableStateOf("LBJReceiver")
|
||||
private var temporaryStatusMessage by mutableStateOf<String?>(null)
|
||||
|
||||
|
||||
private var targetDeviceName = "LBJReceiver"
|
||||
|
||||
|
||||
private val requestPermissions = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { permissions ->
|
||||
|
||||
val bluetoothPermissionsGranted = permissions.filter { it.key.contains("BLUETOOTH") }.all { it.value }
|
||||
val locationPermissionsGranted = permissions.filter { it.key.contains("LOCATION") }.all { it.value }
|
||||
|
||||
if (bluetoothPermissionsGranted && locationPermissionsGranted) {
|
||||
Log.d(TAG, "Permissions granted")
|
||||
|
||||
startScan()
|
||||
} else {
|
||||
Log.e(TAG, "Missing permissions: $permissions")
|
||||
deviceStatus = "需要蓝牙和位置权限"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val scanCallback = object : ScanCallback() {
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
val device = result.device
|
||||
val deviceName = device.name ?: "未知设备"
|
||||
val deviceAddress = device.address
|
||||
|
||||
Log.d(TAG, "Found device name=$deviceName address=$deviceAddress")
|
||||
|
||||
val existingDevice = scanResults.find { it.device.address == deviceAddress }
|
||||
if (existingDevice == null) {
|
||||
scanResults.add(result)
|
||||
updateDeviceList()
|
||||
|
||||
|
||||
if (deviceName == targetDeviceName) {
|
||||
Log.d(TAG, "Found target=$targetDeviceName, connecting")
|
||||
bleClient.stopScan()
|
||||
connectToDevice(device)
|
||||
showConnectionDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScanFailed(errorCode: Int) {
|
||||
Log.e(TAG, "BLE scan failed code=$errorCode")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
||||
requestPermissions.launch(arrayOf(
|
||||
Manifest.permission.BLUETOOTH,
|
||||
Manifest.permission.BLUETOOTH_ADMIN,
|
||||
Manifest.permission.BLUETOOTH_CONNECT,
|
||||
Manifest.permission.BLUETOOTH_SCAN,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
))
|
||||
|
||||
|
||||
bleClient.setTrainInfoCallback { jsonData ->
|
||||
handleTrainInfo(jsonData)
|
||||
}
|
||||
|
||||
|
||||
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 = 2
|
||||
tileFileSystemThreads = 2
|
||||
|
||||
setUserAgentValue("LBJReceiver/1.0")
|
||||
}
|
||||
|
||||
Log.d(TAG, "OSM cache configured")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "OSM cache config failed", e)
|
||||
}
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
LBJReceiverTheme {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
MainContent(
|
||||
deviceStatus = deviceStatus,
|
||||
isConnected = bleClient.isConnected(),
|
||||
isScanning = isScanning,
|
||||
currentTab = currentTab,
|
||||
onTabChange = { tab -> currentTab = tab },
|
||||
onConnectClick = { showConnectionDialog = true },
|
||||
|
||||
|
||||
latestRecord = latestRecord,
|
||||
recentRecords = recentRecords,
|
||||
lastUpdateTime = lastUpdateTime,
|
||||
temporaryStatusMessage = temporaryStatusMessage,
|
||||
onRecordClick = { record ->
|
||||
Log.d(TAG, "Record clicked train=${record.train}")
|
||||
},
|
||||
onClearMonitorLog = {
|
||||
recentRecords = emptyList()
|
||||
temporaryStatusMessage = null
|
||||
},
|
||||
|
||||
|
||||
allRecords = if (trainRecordManager.getFilteredRecords().isNotEmpty())
|
||||
trainRecordManager.getFilteredRecords() else trainRecordManager.getAllRecords(),
|
||||
recordCount = trainRecordManager.getRecordCount(),
|
||||
filterTrain = filterTrain,
|
||||
filterRoute = filterRoute,
|
||||
filterDirection = filterDirection,
|
||||
onFilterChange = { train, route, direction ->
|
||||
filterTrain = train
|
||||
filterRoute = route
|
||||
filterDirection = direction
|
||||
trainRecordManager.setFilter(train, route, direction)
|
||||
},
|
||||
onClearFilter = {
|
||||
filterTrain = ""
|
||||
filterRoute = ""
|
||||
filterDirection = "全部"
|
||||
trainRecordManager.clearFilter()
|
||||
},
|
||||
onClearRecords = {
|
||||
scope.launch {
|
||||
trainRecordManager.clearRecords()
|
||||
recentRecords = emptyList()
|
||||
latestRecord = null
|
||||
temporaryStatusMessage = null
|
||||
}
|
||||
},
|
||||
onExportRecords = {
|
||||
scope.launch {
|
||||
exportRecordsToCSV()
|
||||
}
|
||||
},
|
||||
onDeleteRecords = { records ->
|
||||
scope.launch {
|
||||
val deletedCount = trainRecordManager.deleteRecords(records)
|
||||
if (deletedCount > 0) {
|
||||
Toast.makeText(
|
||||
this@MainActivity,
|
||||
"已删除 $deletedCount 条记录",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
if (records.contains(latestRecord)) {
|
||||
latestRecord = null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
deviceName = settingsDeviceName,
|
||||
onDeviceNameChange = { newName -> settingsDeviceName = newName },
|
||||
onApplySettings = {
|
||||
|
||||
|
||||
Toast.makeText(this, "设备名称 '${settingsDeviceName}' 已保存,下次连接时生效", Toast.LENGTH_LONG).show()
|
||||
Log.d(TAG, "Applied settings deviceName=${settingsDeviceName}")
|
||||
},
|
||||
locoInfoUtil = locoInfoUtil
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun connectToDevice(device: BluetoothDevice) {
|
||||
deviceStatus = "正在连接..."
|
||||
Log.d(TAG, "Connecting to device name=${device.name ?: "Unknown"} address=${device.address}")
|
||||
|
||||
bleClient.connect(device.address) { connected ->
|
||||
if (connected) {
|
||||
deviceStatus = "已连接"
|
||||
Log.d(TAG, "Connected to device name=${device.name ?: "Unknown"}")
|
||||
} else {
|
||||
deviceStatus = "连接失败或已断开连接"
|
||||
Log.e(TAG, "Connection failed name=${device.name ?: "Unknown"}")
|
||||
}
|
||||
}
|
||||
|
||||
deviceAddress = device.address
|
||||
stopScan()
|
||||
}
|
||||
|
||||
|
||||
private fun handleTrainInfo(jsonData: JSONObject) {
|
||||
Log.d(TAG, "Received train data=${jsonData.toString().take(50)}...")
|
||||
|
||||
runOnUiThread {
|
||||
try {
|
||||
val isTestData = jsonData.optBoolean("test_flag", false)
|
||||
lastUpdateTime = Date()
|
||||
|
||||
if (isTestData) {
|
||||
Log.i(TAG, "Received keep-alive signal")
|
||||
forceUiRefresh()
|
||||
} else {
|
||||
temporaryStatusMessage = null
|
||||
|
||||
val record = trainRecordManager.addRecord(jsonData)
|
||||
Log.d(TAG, "Added record train=${record.train} direction=${record.direction}")
|
||||
|
||||
|
||||
latestRecord = record
|
||||
|
||||
val newList = mutableListOf<TrainRecord>()
|
||||
newList.add(record)
|
||||
newList.addAll(recentRecords.filterNot { it.train == record.train && it.time == record.time })
|
||||
recentRecords = newList.take(10)
|
||||
|
||||
Log.d(TAG, "Updated UI train=${record.train}")
|
||||
forceUiRefresh()
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Train data error: ${e.message}")
|
||||
e.printStackTrace()
|
||||
temporaryStatusMessage = null
|
||||
|
||||
forceUiRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun forceUiRefresh() {
|
||||
Log.d(TAG, "Refreshing UI train=${latestRecord?.train}")
|
||||
}
|
||||
|
||||
|
||||
private fun exportRecordsToCSV() {
|
||||
val records = trainRecordManager.getFilteredRecords()
|
||||
val file = trainRecordManager.exportToCsv(records)
|
||||
if (file != null) {
|
||||
try {
|
||||
|
||||
val uri = FileProvider.getUriForFile(
|
||||
this,
|
||||
"${applicationContext.packageName}.provider",
|
||||
file
|
||||
)
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/csv"
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
startActivity(Intent.createChooser(intent, "分享CSV文件"))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "CSV export failed: ${e.message}")
|
||||
Toast.makeText(this, "导出失败: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "导出CSV文件失败", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateTemporaryStatusMessage(message: String) {
|
||||
temporaryStatusMessage = message
|
||||
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (temporaryStatusMessage == message) {
|
||||
temporaryStatusMessage = null
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
|
||||
private fun startScan() {
|
||||
isScanning = true
|
||||
foundDevices = emptyList()
|
||||
val targetDeviceName = settingsDeviceName.ifBlank { null }
|
||||
Log.d(TAG, "Starting BLE scan target=${targetDeviceName ?: "Any"}")
|
||||
|
||||
bleClient.scanDevices(targetDeviceName) { device ->
|
||||
if (!foundDevices.any { it.address == device.address }) {
|
||||
Log.d(TAG, "Found device name=${device.name ?: "Unknown"} address=${device.address}")
|
||||
foundDevices = foundDevices + device
|
||||
|
||||
if (targetDeviceName != null && device.name == targetDeviceName) {
|
||||
Log.d(TAG, "Found target=$targetDeviceName, connecting")
|
||||
stopScan()
|
||||
connectToDevice(device)
|
||||
} else if (!foundDevices.any { it.address == device.address }) {
|
||||
showConnectionDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun stopScan() {
|
||||
isScanning = false
|
||||
bleClient.stopScan()
|
||||
Log.d(TAG, "Stopped BLE scan")
|
||||
}
|
||||
|
||||
|
||||
private fun updateDeviceList() {
|
||||
foundDevices = scanResults.map { it.device }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainContent(
|
||||
deviceStatus: String,
|
||||
isConnected: Boolean,
|
||||
isScanning: Boolean,
|
||||
currentTab: Int,
|
||||
onTabChange: (Int) -> Unit,
|
||||
onConnectClick: () -> Unit,
|
||||
|
||||
|
||||
latestRecord: TrainRecord?,
|
||||
recentRecords: List<TrainRecord>,
|
||||
lastUpdateTime: Date?,
|
||||
temporaryStatusMessage: String? = null,
|
||||
onRecordClick: (TrainRecord) -> Unit,
|
||||
onClearMonitorLog: () -> Unit,
|
||||
|
||||
|
||||
allRecords: List<TrainRecord>,
|
||||
recordCount: Int,
|
||||
filterTrain: String,
|
||||
filterRoute: String,
|
||||
filterDirection: String,
|
||||
onFilterChange: (String, String, String) -> Unit,
|
||||
onClearFilter: () -> Unit,
|
||||
onClearRecords: () -> Unit,
|
||||
onExportRecords: () -> Unit,
|
||||
onDeleteRecords: (List<TrainRecord>) -> Unit,
|
||||
|
||||
|
||||
deviceName: String,
|
||||
onDeviceNameChange: (String) -> Unit,
|
||||
onApplySettings: () -> Unit,
|
||||
|
||||
|
||||
locoInfoUtil: LocoInfoUtil
|
||||
) {
|
||||
val statusColor = if (isConnected) Color(0xFF4CAF50) else Color(0xFFFF5722)
|
||||
|
||||
|
||||
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}小时前"
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
} else {
|
||||
timeSinceLastUpdate.value = null
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("LBJReceiver") },
|
||||
actions = {
|
||||
|
||||
timeSinceLastUpdate.value?.let { time ->
|
||||
Text(
|
||||
text = time,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.background(
|
||||
color = statusColor,
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
IconButton(onClick = onConnectClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bluetooth,
|
||||
contentDescription = "连接蓝牙设备"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
NavigationBarItem(
|
||||
selected = currentTab == 0,
|
||||
onClick = { onTabChange(0) },
|
||||
icon = { Icon(Icons.Filled.DirectionsRailway, "记录") },
|
||||
label = { Text("列车记录") }
|
||||
)
|
||||
|
||||
NavigationBarItem(
|
||||
selected = currentTab == 3,
|
||||
onClick = { onTabChange(3) },
|
||||
icon = { Icon(Icons.Filled.LocationOn, "地图") },
|
||||
label = { Text("位置地图") }
|
||||
)
|
||||
|
||||
NavigationBarItem(
|
||||
selected = currentTab == 2,
|
||||
onClick = { onTabChange(2) },
|
||||
icon = { Icon(Icons.Filled.Settings, "设置") },
|
||||
label = { Text("设置") }
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
when (currentTab) {
|
||||
0 -> HistoryScreen(
|
||||
records = allRecords,
|
||||
latestRecord = latestRecord,
|
||||
lastUpdateTime = lastUpdateTime,
|
||||
temporaryStatusMessage = temporaryStatusMessage,
|
||||
locoInfoUtil = locoInfoUtil,
|
||||
onClearRecords = onClearRecords,
|
||||
onExportRecords = onExportRecords,
|
||||
onRecordClick = onRecordClick,
|
||||
onClearLog = onClearMonitorLog,
|
||||
onDeleteRecords = onDeleteRecords
|
||||
)
|
||||
2 -> SettingsScreen(
|
||||
deviceName = deviceName,
|
||||
onDeviceNameChange = onDeviceNameChange,
|
||||
onApplySettings = onApplySettings,
|
||||
)
|
||||
3 -> MapScreen(
|
||||
records = if (allRecords.isNotEmpty()) allRecords else recentRecords,
|
||||
onCenterMap = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConnectionDialog(
|
||||
isScanning: Boolean,
|
||||
devices: List<BluetoothDevice>,
|
||||
onDismiss: () -> Unit,
|
||||
onScan: () -> Unit,
|
||||
onConnect: (BluetoothDevice) -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("连接设备") },
|
||||
text = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Button(
|
||||
onClick = onScan,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(if (isScanning) "停止扫描" else "扫描设备")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (isScanning) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
if (devices.isEmpty()) {
|
||||
Text("未找到设备")
|
||||
} else {
|
||||
Column {
|
||||
devices.forEach { device ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clickable { onConnect(device) }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = device.name ?: "未知设备",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = device.address,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun Date.toSimpleFormat(): String {
|
||||
val sdf = java.text.SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||
return sdf.format(this)
|
||||
}
|
||||
152
app/src/main/java/receiver/lbj/model/TrainRecord.kt
Normal file
@@ -0,0 +1,152 @@
|
||||
package receiver.lbj.model
|
||||
|
||||
import android.util.Log
|
||||
import org.json.JSONObject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import receiver.lbj.util.LocationUtils
|
||||
|
||||
class TrainRecord(jsonData: JSONObject? = null) {
|
||||
companion object {
|
||||
const val TAG = "TrainRecord"
|
||||
}
|
||||
|
||||
var timestamp: Date = Date()
|
||||
var train: String = ""
|
||||
var direction: Int = 0
|
||||
var speed: String = ""
|
||||
var position: String = ""
|
||||
var time: String = ""
|
||||
var loco: String = ""
|
||||
var locoType: String = ""
|
||||
var lbjClass: String = ""
|
||||
var route: String = ""
|
||||
var positionInfo: String = ""
|
||||
var rssi: Double = 0.0
|
||||
|
||||
|
||||
private var _coordinates: GeoPoint? = null
|
||||
|
||||
init {
|
||||
jsonData?.let {
|
||||
try {
|
||||
if (jsonData.has("timestamp")) {
|
||||
|
||||
timestamp = Date(jsonData.getLong("timestamp"))
|
||||
}
|
||||
updateFromJson(it)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initialize TrainRecord from JSON: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFromJson(jsonData: JSONObject) {
|
||||
try {
|
||||
Log.d(TAG, "Parsing JSON: ${jsonData.toString().take(100)}...")
|
||||
|
||||
train = jsonData.optString("train", "")
|
||||
direction = jsonData.optInt("dir", 0)
|
||||
speed = jsonData.optString("speed", "")
|
||||
position = jsonData.optString("pos", "")
|
||||
time = jsonData.optString("time", "")
|
||||
loco = jsonData.optString("loco", "")
|
||||
locoType = jsonData.optString("loco_type", "")
|
||||
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")
|
||||
} 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) { }
|
||||
try { position = jsonData.optString("pos", "") } catch (e: Exception) { }
|
||||
try { time = jsonData.optString("time", "") } catch (e: Exception) { }
|
||||
|
||||
Log.d(TAG, "Attempting field-level parse: train=$train, dir=$direction")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getCoordinates(): GeoPoint? {
|
||||
|
||||
if (_coordinates != null) {
|
||||
return _coordinates
|
||||
}
|
||||
|
||||
|
||||
_coordinates = LocationUtils.parsePositionInfo(positionInfo)
|
||||
return _coordinates
|
||||
}
|
||||
private fun isValidValue(value: String): Boolean {
|
||||
val trimmed = value.trim()
|
||||
return trimmed.isNotEmpty() &&
|
||||
trimmed != "NUL" &&
|
||||
trimmed != "<NUL>" &&
|
||||
trimmed != "NA" &&
|
||||
trimmed != "<NA>" &&
|
||||
!trimmed.all { it == '*' }
|
||||
}
|
||||
|
||||
fun toMap(): Map<String, String> {
|
||||
val directionText = when (direction) {
|
||||
1 -> "下行"
|
||||
3 -> "上行"
|
||||
else -> "未知"
|
||||
}
|
||||
|
||||
|
||||
val trainDisplay = if (isValidValue(lbjClass) && isValidValue(train)) {
|
||||
"${lbjClass.trim()}${train.trim()}"
|
||||
} else if (isValidValue(lbjClass)) {
|
||||
lbjClass.trim()
|
||||
} else if (isValidValue(train)) {
|
||||
train.trim()
|
||||
} else ""
|
||||
|
||||
val map = mutableMapOf<String, String>()
|
||||
|
||||
|
||||
if (trainDisplay.isNotEmpty()) map["train"] = trainDisplay
|
||||
if (directionText != "未知") map["direction"] = directionText
|
||||
if (isValidValue(speed)) map["speed"] = "速度: ${speed.trim()} km/h"
|
||||
if (isValidValue(position)) map["position"] = "位置: ${position.trim()} km"
|
||||
if (isValidValue(time)) map["time"] = "列车时间: ${time.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()}"
|
||||
}
|
||||
if (rssi != 0.0) map["rssi"] = "信号强度: $rssi dBm"
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
|
||||
fun toJSON(): JSONObject {
|
||||
val json = JSONObject()
|
||||
json.put("timestamp", timestamp.time)
|
||||
json.put("train", train)
|
||||
json.put("dir", direction)
|
||||
json.put("speed", speed)
|
||||
json.put("pos", position)
|
||||
json.put("time", time)
|
||||
json.put("loco", loco)
|
||||
json.put("loco_type", locoType)
|
||||
json.put("lbj_class", lbjClass)
|
||||
json.put("route", route)
|
||||
json.put("position_info", positionInfo)
|
||||
json.put("rssi", rssi)
|
||||
return json
|
||||
}
|
||||
}
|
||||
216
app/src/main/java/receiver/lbj/model/TrainRecordManager.kt
Normal file
@@ -0,0 +1,216 @@
|
||||
package receiver.lbj.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class TrainRecordManager(private val context: Context) {
|
||||
companion object {
|
||||
const val TAG = "TrainRecordManager"
|
||||
const val MAX_RECORDS = 1000
|
||||
private const val PREFS_NAME = "train_records"
|
||||
private const val KEY_RECORDS = "records"
|
||||
}
|
||||
|
||||
|
||||
private val trainRecords = CopyOnWriteArrayList<TrainRecord>()
|
||||
private val recordCount = AtomicInteger(0)
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
init {
|
||||
loadRecords()
|
||||
}
|
||||
|
||||
|
||||
private var filterTrain: String = ""
|
||||
private var filterRoute: String = ""
|
||||
private var filterDirection: String = "全部"
|
||||
|
||||
|
||||
fun addRecord(jsonData: JSONObject): TrainRecord {
|
||||
val record = TrainRecord(jsonData)
|
||||
trainRecords.add(0, record)
|
||||
|
||||
|
||||
while (trainRecords.size > MAX_RECORDS) {
|
||||
trainRecords.removeAt(trainRecords.size - 1)
|
||||
}
|
||||
|
||||
recordCount.incrementAndGet()
|
||||
saveRecords()
|
||||
return record
|
||||
}
|
||||
|
||||
|
||||
fun getAllRecords(): List<TrainRecord> {
|
||||
return trainRecords
|
||||
}
|
||||
|
||||
|
||||
fun getFilteredRecords(): List<TrainRecord> {
|
||||
if (filterTrain.isEmpty() && filterRoute.isEmpty() && filterDirection == "全部") {
|
||||
return trainRecords
|
||||
}
|
||||
|
||||
return trainRecords.filter { record ->
|
||||
matchFilter(record)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun matchFilter(record: TrainRecord): Boolean {
|
||||
|
||||
if (filterTrain.isNotEmpty() && !record.train.contains(filterTrain)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (filterRoute.isNotEmpty() && !record.route.contains(filterRoute)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (filterDirection != "全部") {
|
||||
val dirText = when (record.direction) {
|
||||
1 -> "下行"
|
||||
3 -> "上行"
|
||||
else -> "未知"
|
||||
}
|
||||
if (dirText != filterDirection) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
fun setFilter(train: String, route: String, direction: String) {
|
||||
filterTrain = train
|
||||
filterRoute = route
|
||||
filterDirection = direction
|
||||
}
|
||||
|
||||
|
||||
fun clearFilter() {
|
||||
filterTrain = ""
|
||||
filterRoute = ""
|
||||
filterDirection = "全部"
|
||||
}
|
||||
|
||||
|
||||
fun clearRecords() {
|
||||
trainRecords.clear()
|
||||
recordCount.set(0)
|
||||
saveRecords()
|
||||
}
|
||||
|
||||
fun deleteRecord(record: TrainRecord): Boolean {
|
||||
val result = trainRecords.remove(record)
|
||||
if (result) {
|
||||
recordCount.decrementAndGet()
|
||||
saveRecords()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun deleteRecords(records: List<TrainRecord>): Int {
|
||||
var deletedCount = 0
|
||||
records.forEach { record ->
|
||||
if (trainRecords.remove(record)) {
|
||||
deletedCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
recordCount.addAndGet(-deletedCount)
|
||||
saveRecords()
|
||||
}
|
||||
return deletedCount
|
||||
}
|
||||
|
||||
private fun saveRecords() {
|
||||
try {
|
||||
val jsonArray = JSONArray()
|
||||
for (record in trainRecords) {
|
||||
jsonArray.put(record.toJSON())
|
||||
}
|
||||
prefs.edit().putString(KEY_RECORDS, jsonArray.toString()).apply()
|
||||
Log.d(TAG, "Saved ${trainRecords.size} records")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to save records: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun loadRecords() {
|
||||
try {
|
||||
val jsonStr = prefs.getString(KEY_RECORDS, "[]")
|
||||
val jsonArray = JSONArray(jsonStr)
|
||||
trainRecords.clear()
|
||||
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
val jsonObject = jsonArray.getJSONObject(i)
|
||||
trainRecords.add(TrainRecord(jsonObject))
|
||||
}
|
||||
|
||||
recordCount.set(trainRecords.size)
|
||||
Log.d(TAG, "Loaded ${trainRecords.size} records")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load records: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun exportToCsv(records: List<TrainRecord>): File? {
|
||||
try {
|
||||
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||
val fileName = "train_records_$timeStamp.csv"
|
||||
|
||||
|
||||
val downloadsDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(downloadsDir, fileName)
|
||||
|
||||
FileWriter(file).use { writer ->
|
||||
|
||||
writer.append("时间戳,列车号,列车类型,方向,速度,位置,时间,机车号,机车类型,路线,位置信息,信号强度\n")
|
||||
|
||||
|
||||
for (record in records) {
|
||||
val map = record.toMap()
|
||||
writer.append(map["timestamp"]).append(",")
|
||||
writer.append(map["train"]).append(",")
|
||||
writer.append(map["lbj_class"]).append(",")
|
||||
writer.append(map["direction"]).append(",")
|
||||
writer.append(map["speed"]?.replace(" km/h", "") ?: "").append(",")
|
||||
writer.append(map["position"]?.replace(" km", "") ?: "").append(",")
|
||||
writer.append(map["time"]).append(",")
|
||||
writer.append(map["loco"]).append(",")
|
||||
writer.append(map["loco_type"]).append(",")
|
||||
writer.append(map["route"]).append(",")
|
||||
writer.append(map["position_info"]).append(",")
|
||||
writer.append(map["rssi"]?.replace(" dBm", "") ?: "").append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return file
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error exporting to CSV: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getRecordCount(): Int {
|
||||
return recordCount.get()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package receiver.lbj.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 receiver.lbj.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"] ?: "未知")
|
||||
}
|
||||
|
||||
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
|
||||
|
||||
DetailItem("接收时间", recordMap["timestamp"] ?: "--")
|
||||
DetailItem("列车时间", recordMap["time"] ?: "--")
|
||||
|
||||
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
|
||||
|
||||
DetailItem("速度", recordMap["speed"] ?: "--")
|
||||
DetailItem("位置", recordMap["position"] ?: "--")
|
||||
DetailItem("位置信息", recordMap["position_info"] ?: "--")
|
||||
|
||||
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
|
||||
|
||||
DetailItem("机车号", recordMap["loco"] ?: "--")
|
||||
DetailItem("机车类型", recordMap["loco_type"] ?: "--")
|
||||
DetailItem("列车类型", recordMap["lbj_class"] ?: "--")
|
||||
|
||||
Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
|
||||
|
||||
DetailItem("路线", recordMap["route"] ?: "--")
|
||||
DetailItem("信号强度", recordMap["rssi"] ?: "--")
|
||||
|
||||
if (coordinates != null) {
|
||||
Divider(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
|
||||
)
|
||||
}
|
||||
}
|
||||
141
app/src/main/java/receiver/lbj/ui/components/TrainInfoCard.kt
Normal file
@@ -0,0 +1,141 @@
|
||||
package receiver.lbj.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
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.unit.sp
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import receiver.lbj.model.TrainRecord
|
||||
|
||||
@Composable
|
||||
fun TrainInfoCard(
|
||||
trainRecord: TrainRecord,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val recordMap = trainRecord.toMap()
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp, horizontal = 6.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = recordMap["train"]?.toString() ?: "",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
val directionStr = recordMap["direction"]?.toString() ?: ""
|
||||
val directionColor = when(directionStr) {
|
||||
"上行" -> MaterialTheme.colorScheme.primary
|
||||
"下行" -> MaterialTheme.colorScheme.secondary
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = directionColor.copy(alpha = 0.1f),
|
||||
modifier = Modifier.padding(horizontal = 2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = directionStr,
|
||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||
fontSize = 12.sp,
|
||||
color = directionColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = recordMap["timestamp"]?.toString()?.split(" ")?.getOrNull(1) ?: "",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "速度: ${recordMap["speed"] ?: ""}",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "位置: ${recordMap["position"] ?: ""}",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
HorizontalDivider(thickness = 0.5.dp)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
CompactInfoItem(label = "机车号", value = recordMap["loco"]?.toString() ?: "")
|
||||
CompactInfoItem(label = "线路", value = recordMap["route"]?.toString() ?: "")
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
CompactInfoItem(label = "类型", value = recordMap["lbj_class"]?.toString() ?: "")
|
||||
CompactInfoItem(label = "信号", value = recordMap["rssi"]?.toString() ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompactInfoItem(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "$label: ",
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = value,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
339
app/src/main/java/receiver/lbj/ui/components/TrainRecordsList.kt
Normal file
@@ -0,0 +1,339 @@
|
||||
package receiver.lbj.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import receiver.lbj.model.TrainRecord
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun TrainRecordsList(
|
||||
records: List<TrainRecord>,
|
||||
onRecordClick: (TrainRecord) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (records.isEmpty()) {
|
||||
Text(
|
||||
text = "暂无历史记录",
|
||||
modifier = Modifier.padding(16.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(vertical = 4.dp, horizontal = 8.dp)
|
||||
) {
|
||||
items(records) { record ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp)
|
||||
.clickable { onRecordClick(record) },
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = record.train,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 15.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
|
||||
val directionText = when (record.direction) {
|
||||
1 -> "下行"
|
||||
3 -> "上行"
|
||||
else -> "未知"
|
||||
}
|
||||
|
||||
val directionColor = when(record.direction) {
|
||||
1 -> MaterialTheme.colorScheme.secondary
|
||||
3 -> MaterialTheme.colorScheme.primary
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = directionColor.copy(alpha = 0.1f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = directionText,
|
||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp),
|
||||
fontSize = 11.sp,
|
||||
color = directionColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
|
||||
Text(
|
||||
text = "位置: ${record.position} km",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Text(
|
||||
text = "${record.speed} km/h",
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
|
||||
val timeStr = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(record.timestamp)
|
||||
Text(
|
||||
text = timeStr,
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TrainRecordsListWithToolbar(
|
||||
records: List<TrainRecord>,
|
||||
onRecordClick: (TrainRecord) -> Unit,
|
||||
onFilterClick: () -> Unit,
|
||||
onExportClick: () -> Unit,
|
||||
onClearClick: () -> Unit,
|
||||
onDeleteRecords: (List<TrainRecord>) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var selectedRecords by remember { mutableStateOf<MutableSet<TrainRecord>>(mutableSetOf()) }
|
||||
var selectionMode by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 3.dp,
|
||||
shadowElevation = 3.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = if (selectionMode) "已选择 ${selectedRecords.size} 条" else "历史记录 (${records.size})",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (selectionMode) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (selectedRecords.isNotEmpty()) {
|
||||
onDeleteRecords(selectedRecords.toList())
|
||||
}
|
||||
selectionMode = false
|
||||
selectedRecords = mutableSetOf()
|
||||
},
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Text("删除")
|
||||
}
|
||||
TextButton(onClick = {
|
||||
selectionMode = false
|
||||
selectedRecords = mutableSetOf()
|
||||
}) {
|
||||
Text("取消")
|
||||
}
|
||||
} else {
|
||||
IconButton(onClick = onFilterClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FilterList,
|
||||
contentDescription = "筛选"
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onExportClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = "导出"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = PaddingValues(vertical = 4.dp, horizontal = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(records.chunked(2)) { rowRecords ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
rowRecords.forEach { record ->
|
||||
val isSelected = selectedRecords.contains(record)
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
if (selectionMode) {
|
||||
if (isSelected) {
|
||||
selectedRecords.remove(record)
|
||||
} else {
|
||||
selectedRecords.add(record)
|
||||
}
|
||||
if (selectedRecords.isEmpty()) {
|
||||
selectionMode = false
|
||||
}
|
||||
} else {
|
||||
onRecordClick(record)
|
||||
}
|
||||
}
|
||||
.padding(vertical = 2.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (selectionMode) {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
selectedRecords.add(record)
|
||||
} else {
|
||||
selectedRecords.remove(record)
|
||||
}
|
||||
if (selectedRecords.isEmpty()) {
|
||||
selectionMode = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = record.train,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 15.sp,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
if (!selectionMode) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
selectionMode = true
|
||||
selectedRecords = mutableSetOf(record)
|
||||
},
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Clear,
|
||||
contentDescription = "删除",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (record.speed.isNotEmpty() || record.position.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
if (record.speed.isNotEmpty()) {
|
||||
Text(
|
||||
text = "${record.speed} km/h",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (record.position.isNotEmpty()) {
|
||||
Text(
|
||||
text = "${record.position} km",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val timeStr = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(record.timestamp)
|
||||
Text(
|
||||
text = timeStr,
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
565
app/src/main/java/receiver/lbj/ui/screens/HistoryScreen.kt
Normal file
@@ -0,0 +1,565 @@
|
||||
package receiver.lbj.ui.screens
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.SignalCellular4Bar
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.positionChange
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import kotlinx.coroutines.delay
|
||||
import org.osmdroid.tileprovider.MapTileProviderBasic
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||
import org.osmdroid.tileprovider.tilesource.XYTileSource
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
|
||||
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
|
||||
import org.osmdroid.views.overlay.TilesOverlay
|
||||
import receiver.lbj.model.TrainRecord
|
||||
import receiver.lbj.util.LocoInfoUtil
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun HistoryScreen(
|
||||
records: List<TrainRecord>,
|
||||
latestRecord: TrainRecord?,
|
||||
lastUpdateTime: Date?,
|
||||
temporaryStatusMessage: String? = null,
|
||||
locoInfoUtil: LocoInfoUtil? = null,
|
||||
onClearRecords: () -> Unit = {},
|
||||
onExportRecords: () -> Unit = {},
|
||||
onRecordClick: (TrainRecord) -> Unit = {},
|
||||
onClearLog: () -> Unit = {},
|
||||
onDeleteRecords: (List<TrainRecord>) -> Unit = {}
|
||||
) {
|
||||
|
||||
val refreshKey = latestRecord?.timestamp?.time ?: 0
|
||||
|
||||
var isInEditMode by remember { mutableStateOf(false) }
|
||||
val selectedRecords = remember { mutableStateListOf<TrainRecord>() }
|
||||
|
||||
val expandedStates = remember { mutableStateMapOf<String, Boolean>() }
|
||||
|
||||
|
||||
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}小时前"
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
val filteredRecords = remember(records, refreshKey) {
|
||||
records
|
||||
}
|
||||
|
||||
fun exitEditMode() {
|
||||
isInEditMode = false
|
||||
selectedRecords.clear()
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedRecords.size) {
|
||||
if (selectedRecords.isEmpty() && isInEditMode) {
|
||||
exitEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.weight(1.0f)
|
||||
) {
|
||||
if (filteredRecords.isEmpty()) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(filteredRecords) { record ->
|
||||
val isSelected = selectedRecords.contains(record)
|
||||
val cardColor = when {
|
||||
isSelected -> MaterialTheme.colorScheme.primaryContainer
|
||||
else -> MaterialTheme.colorScheme.surface
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = cardColor
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
if (isInEditMode) {
|
||||
if (isSelected) {
|
||||
selectedRecords.remove(record)
|
||||
} else {
|
||||
selectedRecords.add(record)
|
||||
}
|
||||
} else {
|
||||
val id = record.timestamp.time.toString()
|
||||
expandedStates[id] =
|
||||
!(expandedStates[id] ?: false)
|
||||
if (record == latestRecord) {
|
||||
onRecordClick(record)
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
if (!isInEditMode) {
|
||||
isInEditMode = true
|
||||
selectedRecords.clear()
|
||||
selectedRecords.add(record)
|
||||
}
|
||||
},
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = true)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
val recordMap = record.toMap()
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val trainDisplay =
|
||||
recordMap["train"]?.toString() ?: "未知列车"
|
||||
|
||||
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}"
|
||||
}
|
||||
|
||||
record.locoType.isNotEmpty() -> record.locoType
|
||||
record.loco.isNotEmpty() -> record.loco
|
||||
else -> ""
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = trainDisplay,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
val directionText = when (record.direction) {
|
||||
1 -> "下"
|
||||
3 -> "上"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
if (directionText.isNotEmpty()) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(2.dp),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(20.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = directionText,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.offset(y = (-2).dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (formattedInfo.isNotEmpty() && formattedInfo != "<NUL>") {
|
||||
Text(
|
||||
text = formattedInfo,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "${record.rssi} dBm",
|
||||
fontSize = 10.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
if (recordMap.containsKey("time")) {
|
||||
recordMap["time"]?.split("\n")?.forEach { timeLine ->
|
||||
Text(
|
||||
text = timeLine,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val routeStr = record.route.trim()
|
||||
val isValidRoute =
|
||||
routeStr.isNotEmpty() && !routeStr.all { it == '*' }
|
||||
|
||||
val position = record.position.trim()
|
||||
val isValidPosition = position.isNotEmpty() &&
|
||||
!position.all { it == '-' || it == '.' } &&
|
||||
position != "<NUL>"
|
||||
|
||||
if (isValidRoute || isValidPosition) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.height(24.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (isValidRoute) {
|
||||
Text(
|
||||
text = "$routeStr",
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.alignByBaseline()
|
||||
)
|
||||
}
|
||||
|
||||
if (isValidPosition) {
|
||||
Text(
|
||||
text = "${position}K",
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.alignByBaseline()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val speed = record.speed.trim()
|
||||
val isValidSpeed = speed.isNotEmpty() &&
|
||||
!speed.all { it == '*' || it == '-' } &&
|
||||
speed != "NUL" &&
|
||||
speed != "<NUL>"
|
||||
if (isValidSpeed) {
|
||||
Text(
|
||||
text = "${speed} km/h",
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (locoInfoUtil != null && record.locoType.isNotEmpty() && record.loco.isNotEmpty()) {
|
||||
val locoInfoText = locoInfoUtil.getLocoInfoDisplay(
|
||||
record.locoType,
|
||||
record.loco
|
||||
)
|
||||
if (locoInfoText != null) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = locoInfoText,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val recordId = record.timestamp.time.toString()
|
||||
if (expandedStates[recordId] == true) {
|
||||
val coordinates = remember { record.getCoordinates() }
|
||||
|
||||
if (coordinates != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
if (coordinates != null) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(220.dp)
|
||||
.padding(vertical = 4.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AndroidView(
|
||||
modifier = Modifier.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {},
|
||||
factory = { context ->
|
||||
MapView(context).apply {
|
||||
setTileSource(TileSourceFactory.MAPNIK)
|
||||
setMultiTouchControls(true)
|
||||
zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
||||
isHorizontalMapRepetitionEnabled =
|
||||
false
|
||||
isVerticalMapRepetitionEnabled =
|
||||
false
|
||||
setHasTransientState(true)
|
||||
setOnTouchListener { v, event ->
|
||||
v.parent?.requestDisallowInterceptTouchEvent(
|
||||
true
|
||||
)
|
||||
false
|
||||
}
|
||||
controller.setZoom(10.0)
|
||||
controller.setCenter(coordinates)
|
||||
this.isTilesScaledToDpi = true
|
||||
this.setUseDataConnection(true)
|
||||
|
||||
try {
|
||||
val railwayTileSource =
|
||||
XYTileSource(
|
||||
"OpenRailwayMap",
|
||||
8, 16,
|
||||
256,
|
||||
".png",
|
||||
arrayOf(
|
||||
"https://a.tiles.openrailwaymap.org/standard/",
|
||||
"https://b.tiles.openrailwaymap.org/standard/",
|
||||
"https://c.tiles.openrailwaymap.org/standard/"
|
||||
),
|
||||
"© OpenRailwayMap contributors, © OpenStreetMap contributors"
|
||||
)
|
||||
|
||||
val railwayProvider =
|
||||
MapTileProviderBasic(context)
|
||||
railwayProvider.tileSource =
|
||||
railwayTileSource
|
||||
|
||||
val railwayOverlay =
|
||||
TilesOverlay(
|
||||
railwayProvider,
|
||||
context
|
||||
)
|
||||
railwayOverlay.loadingBackgroundColor =
|
||||
android.graphics.Color.TRANSPARENT
|
||||
railwayOverlay.loadingLineColor =
|
||||
android.graphics.Color.TRANSPARENT
|
||||
|
||||
overlays.add(railwayOverlay)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
val locationProvider =
|
||||
GpsMyLocationProvider(
|
||||
context
|
||||
).apply {
|
||||
locationUpdateMinDistance =
|
||||
10f
|
||||
locationUpdateMinTime =
|
||||
1000
|
||||
}
|
||||
|
||||
MyLocationNewOverlay(
|
||||
locationProvider,
|
||||
this
|
||||
).apply {
|
||||
enableMyLocation()
|
||||
|
||||
}.also { overlays.add(it) }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
val marker = Marker(this)
|
||||
marker.position = coordinates
|
||||
|
||||
val latStr = String.format(
|
||||
"%.4f",
|
||||
coordinates.latitude
|
||||
)
|
||||
val lonStr = String.format(
|
||||
"%.4f",
|
||||
coordinates.longitude
|
||||
)
|
||||
val coordStr =
|
||||
"${latStr}°N, ${lonStr}°E"
|
||||
marker.title =
|
||||
recordMap["train"]?.toString()
|
||||
?: "列车"
|
||||
|
||||
marker.snippet = coordStr
|
||||
|
||||
marker.setInfoWindowAnchor(
|
||||
Marker.ANCHOR_CENTER,
|
||||
0f
|
||||
)
|
||||
|
||||
overlays.add(marker)
|
||||
marker.showInfoWindow()
|
||||
}
|
||||
},
|
||||
update = { mapView ->
|
||||
mapView.invalidate()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (recordMap.containsKey("position_info")) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = recordMap["position_info"] ?: "",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isInEditMode) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
IconButton(onClick = { exitEditMode() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "取消",
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
"已选择 ${selectedRecords.size} 条记录",
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (selectedRecords.isNotEmpty()) {
|
||||
onDeleteRecords(selectedRecords.toList())
|
||||
exitEditMode()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "删除所选记录",
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
559
app/src/main/java/receiver/lbj/ui/screens/MapScreen.kt
Normal file
@@ -0,0 +1,559 @@
|
||||
package receiver.lbj.ui.screens
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.util.Log
|
||||
import android.location.LocationManager
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.MyLocation
|
||||
import androidx.compose.material.icons.filled.Layers
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import kotlinx.coroutines.launch
|
||||
import org.osmdroid.config.Configuration
|
||||
import org.osmdroid.tileprovider.MapTileProviderBasic
|
||||
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||
import org.osmdroid.tileprovider.tilesource.XYTileSource
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.*
|
||||
import org.osmdroid.views.overlay.compass.CompassOverlay
|
||||
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider
|
||||
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
|
||||
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
|
||||
import receiver.lbj.model.TrainRecord
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
|
||||
@Composable
|
||||
fun MapScreen(
|
||||
records: List<TrainRecord>,
|
||||
onCenterMap: () -> Unit = {},
|
||||
onLocationError: (String) -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
|
||||
val osmCacheDir = File(context.cacheDir, "osm").apply { mkdirs() }
|
||||
val tileCache = File(osmCacheDir, "tiles").apply { mkdirs() }
|
||||
|
||||
|
||||
Configuration.getInstance().apply {
|
||||
userAgentValue = context.packageName
|
||||
load(context, context.getSharedPreferences("osmdroid", Context.MODE_PRIVATE))
|
||||
osmdroidBasePath = osmCacheDir
|
||||
osmdroidTileCache = tileCache
|
||||
expirationOverrideDuration = 86400000L
|
||||
gpsWaitTime = 0L
|
||||
tileDownloadThreads = 2
|
||||
tileFileSystemThreads = 2
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
onLocationError("地图初始化失败:${e.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val validRecords = records.filter { it.getCoordinates() != null }
|
||||
|
||||
val defaultPosition = GeoPoint(39.0851, 117.2015)
|
||||
|
||||
var isMapInitialized by remember { mutableStateOf(false) }
|
||||
val mapViewRef = remember { mutableStateOf<MapView?>(null) }
|
||||
|
||||
|
||||
|
||||
val railwayOverlayRef = remember { mutableStateOf<TilesOverlay?>(null) }
|
||||
val myLocationOverlayRef = remember { mutableStateOf<MyLocationNewOverlay?>(null) }
|
||||
var currentLocation by remember { mutableStateOf<GeoPoint?>(null) }
|
||||
var showDetailDialog by remember { mutableStateOf(false) }
|
||||
var selectedRecord by remember { mutableStateOf<TrainRecord?>(null) }
|
||||
var dialogPosition by remember { mutableStateOf<GeoPoint?>(null) }
|
||||
|
||||
var railwayLayerVisible by remember { mutableStateOf(true) }
|
||||
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
try {
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
mapViewRef.value?.onResume()
|
||||
myLocationOverlayRef.value?.enableMyLocation()
|
||||
}
|
||||
Lifecycle.Event.ON_PAUSE -> {
|
||||
mapViewRef.value?.onPause()
|
||||
myLocationOverlayRef.value?.disableMyLocation()
|
||||
}
|
||||
Lifecycle.Event.ON_DESTROY -> {
|
||||
mapViewRef.value?.onDetach()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
|
||||
onDispose {
|
||||
try {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
myLocationOverlayRef.value?.disableMyLocation()
|
||||
mapViewRef.value?.onDetach()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun updateMarkers() {
|
||||
val mapView = mapViewRef.value ?: return
|
||||
|
||||
|
||||
mapView.overlays.removeAll { it is Marker }
|
||||
|
||||
|
||||
validRecords.forEach { record ->
|
||||
record.getCoordinates()?.let { point ->
|
||||
val marker = Marker(mapView).apply {
|
||||
position = point
|
||||
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
|
||||
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"
|
||||
snippet = coordStr
|
||||
|
||||
setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f)
|
||||
|
||||
setOnMarkerClickListener { clickedMarker, _ ->
|
||||
selectedRecord = record
|
||||
dialogPosition = point
|
||||
showDetailDialog = true
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
mapView.overlays.add(marker)
|
||||
marker.showInfoWindow()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mapView.invalidate()
|
||||
|
||||
|
||||
if (!isMapInitialized && validRecords.isNotEmpty()) {
|
||||
validRecords.firstOrNull()?.getCoordinates()?.let { point ->
|
||||
mapView.controller.setZoom(12.0)
|
||||
mapView.controller.setCenter(point)
|
||||
isMapInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun updateRailwayLayerVisibility(visible: Boolean) {
|
||||
railwayOverlayRef.value?.let { overlay ->
|
||||
overlay.isEnabled = visible
|
||||
|
||||
if (!visible) {
|
||||
|
||||
val transparentFilter = PorterDuffColorFilter(
|
||||
Color.argb(0, 255, 255, 255),
|
||||
PorterDuff.Mode.SRC_IN
|
||||
)
|
||||
overlay.setColorFilter(transparentFilter)
|
||||
} else {
|
||||
|
||||
overlay.setColorFilter(null)
|
||||
}
|
||||
mapViewRef.value?.invalidate()
|
||||
Log.d("MapScreen", "OpenRailwayMap layer ${if (visible) "shown" else "hidden"}")
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
try {
|
||||
MapView(ctx).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
|
||||
setTileSource(TileSourceFactory.MAPNIK)
|
||||
setMultiTouchControls(true)
|
||||
zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
||||
isTilesScaledToDpi = true
|
||||
setUseDataConnection(true)
|
||||
minZoomLevel = 4.0
|
||||
maxZoomLevel = 18.0
|
||||
|
||||
|
||||
|
||||
|
||||
try {
|
||||
val provider = MapTileProviderBasic(ctx)
|
||||
provider.tileSource = TileSourceFactory.MAPNIK
|
||||
val tileOverlay = TilesOverlay(provider, ctx)
|
||||
tileOverlay.loadingBackgroundColor = Color.TRANSPARENT
|
||||
tileOverlay.loadingLineColor = Color.TRANSPARENT
|
||||
overlays.add(tileOverlay)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
|
||||
|
||||
val railwayTileSource = XYTileSource(
|
||||
"OpenRailwayMap",
|
||||
8, 16,
|
||||
256,
|
||||
".png",
|
||||
arrayOf(
|
||||
"https://a.tiles.openrailwaymap.org/standard/",
|
||||
"https://b.tiles.openrailwaymap.org/standard/",
|
||||
"https://c.tiles.openrailwaymap.org/standard/"
|
||||
),
|
||||
"© OpenRailwayMap contributors, © OpenStreetMap contributors"
|
||||
)
|
||||
|
||||
|
||||
val railwayProvider = MapTileProviderBasic(ctx)
|
||||
railwayProvider.tileSource = railwayTileSource
|
||||
|
||||
|
||||
val railwayOverlay = TilesOverlay(railwayProvider, ctx)
|
||||
|
||||
|
||||
val railwayColorFilter = PorterDuffColorFilter(
|
||||
Color.rgb(0, 51, 153),
|
||||
PorterDuff.Mode.MULTIPLY
|
||||
)
|
||||
railwayOverlay.setColorFilter(railwayColorFilter)
|
||||
railwayOverlay.loadingBackgroundColor = Color.TRANSPARENT
|
||||
railwayOverlay.loadingLineColor = Color.TRANSPARENT
|
||||
|
||||
|
||||
overlays.add(railwayOverlay)
|
||||
|
||||
railwayOverlayRef.value = railwayOverlay
|
||||
|
||||
Log.d("MapScreen", "OpenRailwayMap layer loaded")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e("MapScreen", "Failed to load OpenRailwayMap layer: ${e.message}")
|
||||
}
|
||||
|
||||
|
||||
controller.setZoom(10.0)
|
||||
|
||||
|
||||
try {
|
||||
|
||||
val locationProvider = GpsMyLocationProvider(ctx).apply {
|
||||
locationUpdateMinDistance = 10f
|
||||
locationUpdateMinTime = 1000
|
||||
}
|
||||
|
||||
val myLocationOverlay = MyLocationNewOverlay(locationProvider, this).apply {
|
||||
enableMyLocation()
|
||||
|
||||
runOnFirstFix {
|
||||
try {
|
||||
myLocation?.let { location ->
|
||||
currentLocation = GeoPoint(location.latitude, location.longitude)
|
||||
|
||||
if (!isMapInitialized) {
|
||||
controller.animateTo(location)
|
||||
|
||||
isMapInitialized = true
|
||||
}
|
||||
} ?: run {
|
||||
|
||||
if (!isMapInitialized) {
|
||||
controller.animateTo(defaultPosition)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
if (!isMapInitialized) {
|
||||
controller.animateTo(defaultPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
overlays.add(myLocationOverlay)
|
||||
myLocationOverlayRef.value = myLocationOverlay
|
||||
|
||||
|
||||
|
||||
|
||||
ScaleBarOverlay(this).apply {
|
||||
setCentred(false)
|
||||
setScaleBarOffset(5, ctx.resources.displayMetrics.heightPixels - 50)
|
||||
setTextSize(10.0f)
|
||||
setEnableAdjustLength(true)
|
||||
setAlignBottom(true)
|
||||
setLineWidth(2.0f)
|
||||
}.also { overlays.add(it) }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
onLocationError("地图组件初始化失败:${e.localizedMessage}")
|
||||
}
|
||||
|
||||
mapViewRef.value = this
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
onLocationError("地图创建失败:${e.localizedMessage}")
|
||||
|
||||
MapView(ctx).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
update = { mapView ->
|
||||
|
||||
coroutineScope.launch {
|
||||
updateMarkers()
|
||||
updateRailwayLayerVisibility(railwayLayerVisible)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if (!isMapInitialized) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.align(Alignment.Center),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
|
||||
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
myLocationOverlayRef.value?.let { overlay ->
|
||||
overlay.enableFollowLocation()
|
||||
overlay.enableMyLocation()
|
||||
overlay.myLocation?.let { location ->
|
||||
mapViewRef.value?.controller?.animateTo(location)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(40.dp),
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MyLocation,
|
||||
contentDescription = "定位",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
railwayLayerVisible = !railwayLayerVisible
|
||||
updateRailwayLayerVisibility(railwayLayerVisible)
|
||||
},
|
||||
modifier = Modifier.size(40.dp),
|
||||
containerColor = if (railwayLayerVisible)
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
||||
else
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
|
||||
contentColor = if (railwayLayerVisible)
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
else
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Layers,
|
||||
contentDescription = "铁路图层",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
mapViewRef.value?.let { mapView ->
|
||||
if (validRecords.isNotEmpty()) {
|
||||
validRecords.firstOrNull()?.getCoordinates()?.let { point ->
|
||||
mapView.controller.animateTo(point)
|
||||
mapView.controller.setZoom(12.0)
|
||||
}
|
||||
} else {
|
||||
mapView.controller.animateTo(defaultPosition)
|
||||
mapView.controller.setZoom(10.0)
|
||||
}
|
||||
}
|
||||
onCenterMap()
|
||||
},
|
||||
modifier = Modifier.size(40.dp),
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Refresh,
|
||||
contentDescription = "居中地图",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(8.dp)
|
||||
.height(32.dp),
|
||||
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = "${validRecords.size}条记录",
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (showDetailDialog && selectedRecord != null) {
|
||||
TrainMarkerDialog(
|
||||
record = selectedRecord!!,
|
||||
position = dialogPosition,
|
||||
onDismiss = { showDetailDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun Context.getCompactMarkerDrawable(color: Int): Drawable {
|
||||
|
||||
val drawable = this.resources.getDrawable(android.R.drawable.ic_menu_mylocation, this.theme)
|
||||
drawable.setTint(color)
|
||||
return drawable
|
||||
}
|
||||
|
||||
|
||||
private fun Int.directionText(): String = when (this) {
|
||||
1 -> "↓"
|
||||
3 -> "↑"
|
||||
else -> "?"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrainMarkerDialog(
|
||||
record: TrainRecord,
|
||||
position: GeoPoint?,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
|
||||
val recordMap = record.toMap()
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = recordMap["train"]?.toString() ?: "列车", style = MaterialTheme.typography.titleLarge)
|
||||
recordMap["direction"]?.let { direction ->
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = direction,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
|
||||
record.toMap().forEach { (key, value) ->
|
||||
if (key != "train" && key != "direction") {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
position?.let {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "坐标: ${String.format("%.6f", it.latitude)}, ${String.format("%.6f", it.longitude)}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("确定")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
252
app/src/main/java/receiver/lbj/ui/screens/MonitorScreen.kt
Normal file
@@ -0,0 +1,252 @@
|
||||
package receiver.lbj.ui.screens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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 receiver.lbj.model.TrainRecord
|
||||
import receiver.lbj.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) }
|
||||
|
||||
|
||||
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}小时前"
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
) {
|
||||
if (latestRecord != null) {
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
.clickable {
|
||||
selectedRecord = latestRecord
|
||||
showDetailDialog = true
|
||||
onRecordClick(latestRecord)
|
||||
}
|
||||
) {
|
||||
|
||||
val recordMap = latestRecord.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
|
||||
)
|
||||
}
|
||||
}
|
||||
38
app/src/main/java/receiver/lbj/ui/screens/SettingsScreen.kt
Normal file
@@ -0,0 +1,38 @@
|
||||
package receiver.lbj.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
deviceName: String,
|
||||
onDeviceNameChange: (String) -> Unit,
|
||||
onApplySettings: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("设置", style = MaterialTheme.typography.headlineMedium)
|
||||
|
||||
OutlinedTextField(
|
||||
value = deviceName,
|
||||
onValueChange = onDeviceNameChange,
|
||||
label = { Text("蓝牙设备名称") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Button(onClick = onApplySettings, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("应用设备名称")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
11
app/src/main/java/receiver/lbj/ui/theme/Color.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package receiver.lbj.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
50
app/src/main/java/receiver/lbj/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,50 @@
|
||||
package receiver.lbj.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LBJReceiverTheme(
|
||||
darkTheme: Boolean = true,
|
||||
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
19
app/src/main/java/receiver/lbj/ui/theme/Type.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package receiver.lbj.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
|
||||
)
|
||||
70
app/src/main/java/receiver/lbj/util/LocationUtils.kt
Normal file
@@ -0,0 +1,70 @@
|
||||
package receiver.lbj.util
|
||||
|
||||
import android.util.Log
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
object LocationUtils {
|
||||
private const val TAG = "LocationUtils"
|
||||
|
||||
|
||||
fun parsePositionInfo(positionInfo: String): GeoPoint? {
|
||||
try {
|
||||
if (positionInfo.isEmpty() || positionInfo == "--") {
|
||||
return null
|
||||
}
|
||||
|
||||
Log.d(TAG, "Parsing position info=$positionInfo")
|
||||
|
||||
|
||||
val parts = positionInfo.split(" ")
|
||||
if (parts.size != 2) {
|
||||
Log.e(TAG, "Invalid position format=$positionInfo")
|
||||
return null
|
||||
}
|
||||
|
||||
val latitude = convertDmsToDecimal(parts[0])
|
||||
val longitude = convertDmsToDecimal(parts[1])
|
||||
|
||||
if (latitude != null && longitude != null) {
|
||||
Log.d(TAG, "Parsed coordinates lat=$latitude lon=$longitude")
|
||||
return GeoPoint(latitude, longitude)
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Position parse error: ${e.message}", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun convertDmsToDecimal(dmsString: String): Double? {
|
||||
try {
|
||||
|
||||
val degreeIndex = dmsString.indexOf('°')
|
||||
if (degreeIndex == -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
val degrees = dmsString.substring(0, degreeIndex).toDouble()
|
||||
|
||||
|
||||
val minuteEndIndex = dmsString.indexOf('′')
|
||||
if (minuteEndIndex == -1) {
|
||||
return degrees
|
||||
}
|
||||
|
||||
val minutes = dmsString.substring(degreeIndex + 1, minuteEndIndex).toDouble()
|
||||
|
||||
|
||||
val decimalDegrees = degrees + (minutes / 60.0)
|
||||
|
||||
return decimalDegrees
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "转换DMS到十进制度出错: ${e.message}", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
117
app/src/main/java/receiver/lbj/util/LocoInfoUtil.kt
Normal file
@@ -0,0 +1,117 @@
|
||||
package receiver.lbj.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
class LocoInfoUtil(private val context: Context) {
|
||||
|
||||
|
||||
data class LocoInfo(
|
||||
val model: String,
|
||||
val start: Int,
|
||||
val end: Int,
|
||||
val owner: String,
|
||||
val alias: String = "",
|
||||
val manufacturer: String = ""
|
||||
)
|
||||
|
||||
|
||||
private var locoData: List<LocoInfo> = emptyList()
|
||||
|
||||
|
||||
suspend fun loadLocoData() = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val inputStream = context.assets.open("loco_info.csv")
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
val data = mutableListOf<LocoInfo>()
|
||||
|
||||
reader.lineSequence().forEach { line ->
|
||||
val fields = line.split(",").map { it.trim() }
|
||||
if (fields.size >= 4) {
|
||||
try {
|
||||
val model = fields[0]
|
||||
val start = fields[1].toInt()
|
||||
val end = fields[2].toInt()
|
||||
val owner = fields[3]
|
||||
val alias = if (fields.size > 4) fields[4] else ""
|
||||
val manufacturer = if (fields.size > 5) fields[5] else ""
|
||||
|
||||
data.add(LocoInfo(model, start, end, owner, alias, manufacturer))
|
||||
} catch (e: Exception) {
|
||||
Log.e("LocoInfoUtil", "CSV parse error line=$line", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader.close()
|
||||
locoData = data
|
||||
Log.d("LocoInfoUtil", "Loaded records=${data.size}")
|
||||
} catch (e: Exception) {
|
||||
Log.e("LocoInfoUtil", "Load CSV failed", e)
|
||||
locoData = emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun refreshData() {
|
||||
loadLocoData()
|
||||
}
|
||||
|
||||
|
||||
fun findLocoInfo(model: String, number: String): LocoInfo? {
|
||||
if (model.isEmpty() || number.isEmpty()) {
|
||||
Log.d("LocoInfoUtil", "Query failed empty model/number")
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
|
||||
val cleanNumber = number.trim().replace("-", "").replace(" ", "")
|
||||
val num = if (cleanNumber.length > 4) {
|
||||
cleanNumber.takeLast(4).toInt()
|
||||
} else {
|
||||
cleanNumber.toInt()
|
||||
}
|
||||
|
||||
locoData.forEach { info ->
|
||||
if (info.model == model) {
|
||||
val inRange = num in info.start..info.end
|
||||
Log.d("LocoInfoUtil", "Checking model=${info.model} range=${info.start}-${info.end} num=$num match=$inRange")
|
||||
if (inRange) {
|
||||
Log.d("LocoInfoUtil", "Matched owner=${info.owner} alias=${info.alias}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return locoData.find { info ->
|
||||
info.model == model && num in info.start..info.end
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("LocoInfoUtil", "Query failed model=$model number=$number", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getLocoInfoDisplay(model: String, number: String): String? {
|
||||
val info = findLocoInfo(model, number) ?: return null
|
||||
|
||||
val sb = StringBuilder()
|
||||
sb.append(info.owner)
|
||||
|
||||
if (info.alias.isNotEmpty()) {
|
||||
sb.append(" - ${info.alias}")
|
||||
}
|
||||
|
||||
if (info.manufacturer.isNotEmpty()) {
|
||||
sb.append(" - ${info.manufacturer}")
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
6
app/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
590
app/src/main/res/raw/loco_info.csv
Normal file
@@ -0,0 +1,590 @@
|
||||
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,中国铁路沈阳局集团有限公司 沈阳机务段;中国铁路上海局集团有限公司 上海机务段,,
|
||||
|
10
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
3
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">LBJ Receiver</string>
|
||||
</resources>
|
||||
5
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.LBJReceiver" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
4
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
|
||||
</full-backup-content>
|
||||
7
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
|
||||
</cloud-backup>
|
||||
|
||||
</data-extraction-rules>
|
||||
5
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-files-path name="external_files" path="." />
|
||||
<external-files-path name="downloads" path="Download/" />
|
||||
</paths>
|
||||
13
app/src/test/java/receiver/lbj/ExampleUnitTest.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package receiver.lbj
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||