Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bf7033c6c | ||
|
|
0f98b6bcf7 | ||
|
|
8894a73999 | ||
|
|
cd4b58e16b | ||
|
|
39effddfc1 | ||
|
|
c4b06f3b3c | ||
|
|
eb33fa7feb | ||
|
|
65bf7b52c6 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -19,4 +19,6 @@ local.properties
|
|||||||
*.keystore
|
*.keystore
|
||||||
*.base64
|
*.base64
|
||||||
docs
|
docs
|
||||||
gradle.properties
|
linux
|
||||||
|
windows
|
||||||
|
android_original
|
||||||
2
.idea/.name
generated
2
.idea/.name
generated
@@ -1 +1 @@
|
|||||||
LBJ Receiver
|
LBJ_Console
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
# LBJ Console
|
# LBJ Console
|
||||||
|
|
||||||
LBJ Console 是一款 Android 应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) device 设备接收并显示列车预警消息,功能包括:
|
LBJ Console 是一款 Android 应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) 设备接收并显示列车预警消息,功能包括:
|
||||||
|
|
||||||
- 接收列车预警消息,支持可选的手机推送通知。
|
- 接收列车预警消息,支持可选的手机推送通知。
|
||||||
- 显示预警消息的 GPS 信息于地图。
|
- 在地图上显示预警消息的 GPS 信息。
|
||||||
- 基于内置数据文件显示机车配属和车次类型。
|
- 基于内置数据文件显示机车配属,机车类型和车次类型。
|
||||||
|
|
||||||
|
|
||||||
## 数据文件
|
## 数据文件
|
||||||
|
|
||||||
LBJ Console 依赖以下数据文件,位于 `app/src/main/assets/` 目录,用于支持机车配属和车次信息的展示:
|
LBJ Console 依赖以下数据文件,位于 `app/src/main/assets/` 目录,用于支持机车配属和车次信息的展示:
|
||||||
- `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注`。
|
- `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注`。
|
||||||
|
- `loco_type_info.csv`:包含机车类型编码信息,格式为 `机车类型编码,机车类型`。
|
||||||
- `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型`。
|
- `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型`。
|
||||||
|
|
||||||
|
|
||||||
# 许可证
|
# 许可证
|
||||||
|
|
||||||
该项目采用 GNU 通用公共许可证 v3.0(GPLv3)授权。该许可证确保软件保持免费和开源,要求任何修改或衍生作品也必须在相同许可证条款下发布。
|
该项目采用 GNU 通用公共许可证 v3.0(GPLv3)授权。该许可证确保软件保持免费和开源,要求任何修改或衍生作品也必须在相同许可证条款下发布。
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId = "org.noxylva.lbjconsole"
|
applicationId = "org.noxylva.lbjconsole"
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 10
|
versionCode = 13
|
||||||
versionName = "0.1.1"
|
versionName = "0.1.3"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
142
app/src/main/assets/loco_type_info.csv
Normal file
142
app/src/main/assets/loco_type_info.csv
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
001,解放
|
||||||
|
003,前进
|
||||||
|
005,建设
|
||||||
|
006,KD7
|
||||||
|
055,蓝箭控车
|
||||||
|
081,东风21
|
||||||
|
101,东风
|
||||||
|
102,东风2
|
||||||
|
103,东风3
|
||||||
|
104,东风4
|
||||||
|
105,东风4客
|
||||||
|
106,东风4C
|
||||||
|
107,东风5
|
||||||
|
108,东风5宽
|
||||||
|
109,东风6
|
||||||
|
110,东风7
|
||||||
|
111,东风8
|
||||||
|
112,东风9
|
||||||
|
113,东风10
|
||||||
|
114,东方红1
|
||||||
|
115,东方红2
|
||||||
|
116,东方红3
|
||||||
|
117,东方红5
|
||||||
|
118,北京
|
||||||
|
119,北京宽
|
||||||
|
120,ND2
|
||||||
|
121,ND3
|
||||||
|
122,ND4
|
||||||
|
123,ND5
|
||||||
|
124,NY5
|
||||||
|
125,NY6
|
||||||
|
126,NY7
|
||||||
|
127,轻油
|
||||||
|
128,东方红21
|
||||||
|
129,东风7B
|
||||||
|
130,东风5S
|
||||||
|
131,东风7C
|
||||||
|
132,东风7S
|
||||||
|
133,工矿1
|
||||||
|
134,工矿1F
|
||||||
|
135,东风4E
|
||||||
|
136,东风7D
|
||||||
|
137,工矿1A
|
||||||
|
138,东风11
|
||||||
|
139,天安
|
||||||
|
140,东风10F
|
||||||
|
141,东风4D
|
||||||
|
142,东风8B
|
||||||
|
143,东风12
|
||||||
|
144,东风7E
|
||||||
|
145,NYJ1
|
||||||
|
146,NZJ1
|
||||||
|
147,NZJ2
|
||||||
|
148,东风4DJ
|
||||||
|
149,新曙光
|
||||||
|
150,神州
|
||||||
|
151,NJ2
|
||||||
|
152,东风7G
|
||||||
|
153,NDJ3
|
||||||
|
157,FXN3D
|
||||||
|
158,东风11G
|
||||||
|
160,HXN3
|
||||||
|
161,HXN5
|
||||||
|
162,HXN3B
|
||||||
|
163,HXN5B
|
||||||
|
167,FXN3B
|
||||||
|
169,FXN3C
|
||||||
|
170,FXN5C
|
||||||
|
171,FXN3-J
|
||||||
|
201,8G
|
||||||
|
202,8K
|
||||||
|
203,6G
|
||||||
|
204,6K
|
||||||
|
205,韶山1
|
||||||
|
206,韶山3
|
||||||
|
207,韶山4
|
||||||
|
208,韶山5
|
||||||
|
209,韶山6
|
||||||
|
210,韶山3B
|
||||||
|
211,韶山7
|
||||||
|
212,韶山8
|
||||||
|
213,韶山7B
|
||||||
|
214,韶山7C
|
||||||
|
215,韶山6B
|
||||||
|
216,韶山9
|
||||||
|
217,韶山7D
|
||||||
|
218,DJ熊猫
|
||||||
|
219,DJ1
|
||||||
|
220,DJ2
|
||||||
|
221,DJF
|
||||||
|
222,蓝箭动车
|
||||||
|
223,先锋号
|
||||||
|
224,韶山7E
|
||||||
|
225,韶山4G
|
||||||
|
226,韶山3C
|
||||||
|
228,天梭
|
||||||
|
229,DJ4和谐
|
||||||
|
230,KTT
|
||||||
|
231,HXD1
|
||||||
|
232,HXD2
|
||||||
|
233,HXD3
|
||||||
|
234,HXD1B
|
||||||
|
235,HXD2B
|
||||||
|
236,HXD3B
|
||||||
|
237,HXD1C
|
||||||
|
238,HXD2C
|
||||||
|
239,HXD3C
|
||||||
|
240,HXD1D
|
||||||
|
241,HXD2D
|
||||||
|
242,HXD3D
|
||||||
|
243,FXD1B
|
||||||
|
244,FXD2B
|
||||||
|
245,FXD1
|
||||||
|
246,FXD3
|
||||||
|
247,FXD1-J
|
||||||
|
248,FXD3-J
|
||||||
|
249,KZ25TA
|
||||||
|
251,KZ25TB
|
||||||
|
252,HXD1D-J
|
||||||
|
254,FXD1H
|
||||||
|
300,雪域神州
|
||||||
|
301,CRH1
|
||||||
|
302,CRH2
|
||||||
|
303,CRH3
|
||||||
|
305,CRH5
|
||||||
|
306,CRH380A
|
||||||
|
307,CRH380B
|
||||||
|
308,CRH380C
|
||||||
|
309,CRH380D
|
||||||
|
310,CRH6A
|
||||||
|
311,CR400AF
|
||||||
|
312,CR400BF
|
||||||
|
313,CR300AF
|
||||||
|
314,CR300BF
|
||||||
|
315,CRH2E
|
||||||
|
316,CRH6F
|
||||||
|
330,CJ1
|
||||||
|
331,CJ2
|
||||||
|
332,CJ3
|
||||||
|
333,CJ4
|
||||||
|
334,CJ5
|
||||||
|
335,CJ6
|
||||||
|
@@ -26,8 +26,8 @@
|
|||||||
"^[Vv1](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨三局及以上图定普通旅客快车"
|
"^[Vv1](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨三局及以上图定普通旅客快车"
|
||||||
"^[Bb2](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨两局图定普通旅客快车"
|
"^[Bb2](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨两局图定普通旅客快车"
|
||||||
"^3(00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨局临时普通旅客快车"
|
"^3(00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨局临时普通旅客快车"
|
||||||
"^[Uu4](00[1-9]|0[1-9]\d|[1-9]\d{2})$","管内图定普通旅客快车四字头"
|
"^[Uu4](00[1-9]|0[1-9]\d|[1-9]\d{2})$","管内图定普通旅客快车"
|
||||||
"^[Xx5]([0-8]\d{2}|9[0-8]\d|99[0-8])$","管内图定普通旅客快车五字头"
|
"^[Xx5](000|1[9][9]|200|3[9][9]|400)$","管内图定普通旅客快车"
|
||||||
"^6(19[0-8]|1[0-8]\d|0[1-9]\d|00[1-9])$","直通普通旅客慢车"
|
"^6(19[0-8]|1[0-8]\d|0[1-9]\d|00[1-9])$","直通普通旅客慢车"
|
||||||
"^(6(20[1-9]|2[1-9]\d|[3-9]\d{2})|7([0-4]\d{2}|5([0-8]\d|9[0-8])))$","管内普通旅客慢车"
|
"^(6(20[1-9]|2[1-9]\d|[3-9]\d{2})|7([0-4]\d{2}|5([0-8]\d|9[0-8])))$","管内普通旅客慢车"
|
||||||
"^(8([0-8]\d{2}|9[0-8]\d|99[0-8])|7(60[1-9]|6[1-9]\d|[7-9]\d{2}))$","通勤列车"
|
"^(8([0-8]\d{2}|9[0-8]\d|99[0-8])|7(60[1-9]|6[1-9]\d|[7-9]\d{2}))$","通勤列车"
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
"^DJ([4-9]\d{2}|40[1-9]|4[1-9]\d)$","动车组检测列车300管内"
|
"^DJ([4-9]\d{2}|40[1-9]|4[1-9]\d)$","动车组检测列车300管内"
|
||||||
"^DJ1(400|[0-3]\d{2})$","动车组检测列车250直通"
|
"^DJ1(400|[0-3]\d{2})$","动车组检测列车250直通"
|
||||||
"^DJ1(40[1-9]|4[1-9]\d|[5-9]\d{2})$","动车组检测列车250管内"
|
"^DJ1(40[1-9]|4[1-9]\d|[5-9]\d{2})$","动车组检测列车250管内"
|
||||||
"^DJ[56]\d{3}$","动车组确认列车直通"
|
"^DJ[56]\d{3}$","直通动车组确认列车"
|
||||||
"^DJ[78]\d{3}$","动车组确认列车管内"
|
"^DJ[78]\d{3}$","管内动车组确认列车"
|
||||||
"^[Ff][GDCZTKgdcztk]?\d{1,4}$","因故折返旅客列车"
|
"^[Ff][GDCZTKgdcztk]?\d{1,4}$","因故折返旅客列车"
|
||||||
"^0[GDCZTKgdcztk]\d{1,4}$","回送图定客车底"
|
"^0[GDCZTKgdcztk]\d{1,4}$","回送图定客车底"
|
||||||
"^00(100|[1-9]\d?)$","有火回送动车组车底"
|
"^00(100|[1-9]\d?)$","有火回送动车组车底"
|
||||||
|
|||||||
|
@@ -110,15 +110,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
private var historyScrollPosition by mutableStateOf(0)
|
private var historyScrollPosition by mutableStateOf(0)
|
||||||
private var historyScrollOffset by mutableStateOf(0)
|
private var historyScrollOffset by mutableStateOf(0)
|
||||||
private var historyCardMapStates by mutableStateOf<Map<String, CardMapView>>(emptyMap())
|
private var historyCardMapStates by mutableStateOf<Map<String, CardMapView>>(emptyMap())
|
||||||
|
private var settingsScrollPosition by mutableStateOf(0)
|
||||||
private var mapCenterPosition by mutableStateOf<Pair<Double, Double>?>(null)
|
private var mapCenterPosition by mutableStateOf<Pair<Double, Double>?>(null)
|
||||||
private var mapZoomLevel by mutableStateOf(10.0)
|
private var mapZoomLevel by mutableStateOf(10.0)
|
||||||
private var mapRailwayLayerVisible by mutableStateOf(true)
|
private var mapRailwayLayerVisible by mutableStateOf(true)
|
||||||
|
|
||||||
private var settingsScrollPosition by mutableStateOf(0)
|
|
||||||
|
|
||||||
private var mergeSettings by mutableStateOf(MergeSettings())
|
private var mergeSettings by mutableStateOf(MergeSettings())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private var targetDeviceName = "LBJReceiver"
|
private var targetDeviceName = "LBJReceiver"
|
||||||
private var specifiedDeviceAddress by mutableStateOf<String?>(null)
|
private var specifiedDeviceAddress by mutableStateOf<String?>(null)
|
||||||
@@ -185,10 +183,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
TrainRecord.initializeLocoTypeUtil(this)
|
||||||
|
|
||||||
loadSettings()
|
loadSettings()
|
||||||
|
|
||||||
|
|
||||||
val permissions = mutableListOf<String>()
|
val permissions = mutableListOf<String>()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
@@ -199,99 +197,23 @@ class MainActivity : ComponentActivity() {
|
|||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
permissions.addAll(arrayOf(
|
permissions.addAll(arrayOf(
|
||||||
Manifest.permission.BLUETOOTH,
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
Manifest.permission.BLUETOOTH_ADMIN
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
permissions.addAll(arrayOf(
|
if (permissions.isNotEmpty()) {
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
requestPermissions.launch(permissions.toTypedArray())
|
||||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
} else {
|
||||||
))
|
startAutoScanAndConnect()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestPermissions.launch(permissions.toTypedArray())
|
Configuration.getInstance().userAgentValue = packageName
|
||||||
|
|
||||||
|
|
||||||
bleClient.setTrainInfoCallback { jsonData ->
|
bleClient.setTrainInfoCallback { jsonData ->
|
||||||
handleTrainInfo(jsonData)
|
handleTrainInfo(jsonData)
|
||||||
}
|
}
|
||||||
|
|
||||||
bleClient.setHighFrequencyReconnect(true)
|
|
||||||
bleClient.setConnectionLostCallback {
|
|
||||||
runOnUiThread {
|
|
||||||
deviceStatus = "连接丢失,正在重连..."
|
|
||||||
showDisconnectButton = false
|
|
||||||
if (showConnectionDialog) {
|
|
||||||
foundDevices = emptyList()
|
|
||||||
startScan()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bleClient.setConnectionSuccessCallback { address ->
|
|
||||||
runOnUiThread {
|
|
||||||
deviceAddress = address
|
|
||||||
deviceStatus = "已连接"
|
|
||||||
showDisconnectButton = true
|
|
||||||
Log.d(TAG, "Connection success callback: address=$address")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
locoInfoUtil.loadLocoData()
|
|
||||||
Log.d(TAG, "Loaded locomotive data")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Load locomotive data failed", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
val osmCacheDir = File(cacheDir, "osm").apply { mkdirs() }
|
|
||||||
val tileCache = File(osmCacheDir, "tiles").apply { mkdirs() }
|
|
||||||
|
|
||||||
|
|
||||||
Configuration.getInstance().apply {
|
|
||||||
userAgentValue = packageName
|
|
||||||
load(this@MainActivity, getSharedPreferences("osmdroid", Context.MODE_PRIVATE))
|
|
||||||
osmdroidBasePath = osmCacheDir
|
|
||||||
osmdroidTileCache = tileCache
|
|
||||||
expirationOverrideDuration = 86400000L * 7
|
|
||||||
tileDownloadThreads = 4
|
|
||||||
tileFileSystemThreads = 4
|
|
||||||
|
|
||||||
setUserAgentValue("LBJConsole/1.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "OSM cache configured")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "OSM cache config failed", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSettings()
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (SettingsActivity.isBackgroundServiceEnabled(this@MainActivity)) {
|
|
||||||
BackgroundService.startService(this@MainActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enableEdgeToEdge()
|
|
||||||
|
|
||||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
|
||||||
isAppearanceLightStatusBars = false
|
|
||||||
}
|
|
||||||
setContent {
|
setContent {
|
||||||
LBJConsoleTheme {
|
LBJConsoleTheme {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -334,7 +256,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
Log.d(TAG, "Auto connect enabled: $enabled")
|
Log.d(TAG, "Auto connect enabled: $enabled")
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
latestRecord = latestRecord,
|
latestRecord = latestRecord,
|
||||||
recentRecords = recentRecords,
|
recentRecords = recentRecords,
|
||||||
lastUpdateTime = lastUpdateTime,
|
lastUpdateTime = lastUpdateTime,
|
||||||
@@ -344,10 +265,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
},
|
},
|
||||||
onClearMonitorLog = {
|
onClearMonitorLog = {
|
||||||
recentRecords.clear()
|
recentRecords.clear()
|
||||||
|
latestRecord = null
|
||||||
|
lastUpdateTime = null
|
||||||
temporaryStatusMessage = null
|
temporaryStatusMessage = null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
allRecords = trainRecordManager.getMixedRecords(),
|
allRecords = trainRecordManager.getMixedRecords(),
|
||||||
mergedRecords = trainRecordManager.getMergedRecords(),
|
mergedRecords = trainRecordManager.getMergedRecords(),
|
||||||
recordCount = trainRecordManager.getRecordCount(),
|
recordCount = trainRecordManager.getRecordCount(),
|
||||||
@@ -499,7 +421,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -582,7 +503,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
|
|
||||||
private fun handleTrainInfo(jsonData: JSONObject) {
|
private fun handleTrainInfo(jsonData: JSONObject) {
|
||||||
Log.d(TAG, "Received train data=${jsonData.toString().take(50)}...")
|
Log.d(TAG, "Received train data=${jsonData.toString()}...")
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
try {
|
try {
|
||||||
@@ -762,7 +683,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val settings = appSettingsRepository.getSettings()
|
val settings = appSettingsRepository.getSettings()
|
||||||
|
|
||||||
settingsDeviceName = settings.deviceName
|
settingsDeviceName = settings.deviceName
|
||||||
targetDeviceName = settingsDeviceName
|
targetDeviceName = settings.deviceName
|
||||||
currentTab = settings.currentTab
|
currentTab = settings.currentTab
|
||||||
historyEditMode = settings.historyEditMode
|
historyEditMode = settings.historyEditMode
|
||||||
|
|
||||||
@@ -845,6 +766,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
Log.d(TAG, "onNewIntent called")
|
||||||
|
currentTab = 0
|
||||||
|
forceUiRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
Log.d(TAG, "App resumed")
|
Log.d(TAG, "App resumed")
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ class NotificationService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isValidValue(trainRecord.position)) {
|
if (isValidValue(trainRecord.position)) {
|
||||||
remoteViews.setTextViewText(R.id.notification_position, "${trainRecord.position.trim()}K")
|
remoteViews.setTextViewText(R.id.notification_position, "${trainRecord.position.trim().removeSuffix(".")}K")
|
||||||
remoteViews.setViewVisibility(R.id.notification_position, View.VISIBLE)
|
remoteViews.setViewVisibility(R.id.notification_position, View.VISIBLE)
|
||||||
} else {
|
} else {
|
||||||
remoteViews.setViewVisibility(R.id.notification_position, View.GONE)
|
remoteViews.setViewVisibility(R.id.notification_position, View.GONE)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [TrainRecordEntity::class, AppSettingsEntity::class],
|
entities = [TrainRecordEntity::class, AppSettingsEntity::class],
|
||||||
version = 3,
|
version = 4,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
abstract class TrainDatabase : RoomDatabase() {
|
abstract class TrainDatabase : RoomDatabase() {
|
||||||
@@ -54,13 +54,89 @@ abstract class TrainDatabase : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Since we can't determine the exact schema change, we'll use fallback migration
|
||||||
|
// This will preserve data where possible while updating the schema
|
||||||
|
|
||||||
|
// Create new table with correct schema
|
||||||
|
database.execSQL("""
|
||||||
|
CREATE TABLE IF NOT EXISTS `app_settings_new` (
|
||||||
|
`id` INTEGER NOT NULL,
|
||||||
|
`deviceName` TEXT NOT NULL DEFAULT 'LBJReceiver',
|
||||||
|
`currentTab` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`historyEditMode` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`historySelectedRecords` TEXT NOT NULL DEFAULT '',
|
||||||
|
`historyExpandedStates` TEXT NOT NULL DEFAULT '',
|
||||||
|
`historyScrollPosition` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`historyScrollOffset` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`settingsScrollPosition` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`mapCenterLat` REAL,
|
||||||
|
`mapCenterLon` REAL,
|
||||||
|
`mapZoomLevel` REAL NOT NULL DEFAULT 10.0,
|
||||||
|
`mapRailwayLayerVisible` INTEGER NOT NULL DEFAULT 1,
|
||||||
|
`specifiedDeviceAddress` TEXT,
|
||||||
|
`searchOrderList` TEXT NOT NULL DEFAULT '',
|
||||||
|
`autoConnectEnabled` INTEGER NOT NULL DEFAULT 1,
|
||||||
|
`backgroundServiceEnabled` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`notificationEnabled` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
// Copy data from old table to new table, handling missing columns
|
||||||
|
try {
|
||||||
|
database.execSQL("""
|
||||||
|
INSERT INTO `app_settings_new` (
|
||||||
|
id, deviceName, currentTab, historyEditMode, historySelectedRecords,
|
||||||
|
historyExpandedStates, historyScrollPosition, historyScrollOffset,
|
||||||
|
settingsScrollPosition, mapCenterLat, mapCenterLon, mapZoomLevel,
|
||||||
|
mapRailwayLayerVisible
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COALESCE(id, 1),
|
||||||
|
COALESCE(deviceName, 'LBJReceiver'),
|
||||||
|
COALESCE(currentTab, 0),
|
||||||
|
COALESCE(historyEditMode, 0),
|
||||||
|
COALESCE(historySelectedRecords, ''),
|
||||||
|
COALESCE(historyExpandedStates, ''),
|
||||||
|
COALESCE(historyScrollPosition, 0),
|
||||||
|
COALESCE(historyScrollOffset, 0),
|
||||||
|
COALESCE(settingsScrollPosition, 0),
|
||||||
|
mapCenterLat,
|
||||||
|
mapCenterLon,
|
||||||
|
COALESCE(mapZoomLevel, 10.0),
|
||||||
|
COALESCE(mapRailwayLayerVisible, 1)
|
||||||
|
FROM `app_settings`
|
||||||
|
""")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If the old table doesn't exist or has different structure, insert default
|
||||||
|
database.execSQL("""
|
||||||
|
INSERT INTO `app_settings_new` (
|
||||||
|
id, deviceName, currentTab, historyEditMode, historySelectedRecords,
|
||||||
|
historyExpandedStates, historyScrollPosition, historyScrollOffset,
|
||||||
|
settingsScrollPosition, mapZoomLevel, mapRailwayLayerVisible,
|
||||||
|
searchOrderList, autoConnectEnabled, backgroundServiceEnabled,
|
||||||
|
notificationEnabled
|
||||||
|
) VALUES (
|
||||||
|
1, 'LBJReceiver', 0, 0, '', '', 0, 0, 0, 10.0, 1, '', 1, 0, 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop old table and rename new table
|
||||||
|
database.execSQL("DROP TABLE IF EXISTS `app_settings`")
|
||||||
|
database.execSQL("ALTER TABLE `app_settings_new` RENAME TO `app_settings`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getDatabase(context: Context): TrainDatabase {
|
fun getDatabase(context: Context): TrainDatabase {
|
||||||
return INSTANCE ?: synchronized(this) {
|
return INSTANCE ?: synchronized(this) {
|
||||||
val instance = Room.databaseBuilder(
|
val instance = Room.databaseBuilder(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
TrainDatabase::class.java,
|
TrainDatabase::class.java,
|
||||||
"train_database"
|
"train_database"
|
||||||
).addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
|
).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4).build()
|
||||||
INSTANCE = instance
|
INSTANCE = instance
|
||||||
instance
|
instance
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
package org.noxylva.lbjconsole.model
|
package org.noxylva.lbjconsole.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import org.osmdroid.util.GeoPoint
|
import org.osmdroid.util.GeoPoint
|
||||||
import org.noxylva.lbjconsole.util.LocationUtils
|
import org.noxylva.lbjconsole.util.LocationUtil
|
||||||
|
import org.noxylva.lbjconsole.util.LocoTypeUtil
|
||||||
|
|
||||||
class TrainRecord(jsonData: JSONObject? = null) {
|
class TrainRecord(jsonData: JSONObject? = null) {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "TrainRecord"
|
const val TAG = "TrainRecord"
|
||||||
private var nextId = 0L
|
private var nextId = 0L
|
||||||
|
private var LocoTypeUtil: LocoTypeUtil? = null
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun generateUniqueId(): String {
|
private fun generateUniqueId(): String {
|
||||||
return "${System.currentTimeMillis()}_${++nextId}"
|
return "${System.currentTimeMillis()}_${++nextId}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun initializeLocoTypeUtil(context: Context) {
|
||||||
|
if (LocoTypeUtil == null) {
|
||||||
|
LocoTypeUtil = LocoTypeUtil(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val uniqueId: String
|
val uniqueId: String
|
||||||
@@ -75,20 +84,25 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
|||||||
position = jsonData.optString("pos", "")
|
position = jsonData.optString("pos", "")
|
||||||
time = jsonData.optString("time", "")
|
time = jsonData.optString("time", "")
|
||||||
loco = jsonData.optString("loco", "")
|
loco = jsonData.optString("loco", "")
|
||||||
locoType = jsonData.optString("loco_type", "")
|
|
||||||
|
locoType = if (loco.isNotEmpty()) {
|
||||||
|
val prefix = if (loco.length >= 3) loco.take(3) else loco
|
||||||
|
LocoTypeUtil?.getLocoTypeByCode(prefix) ?: ""
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
lbjClass = jsonData.optString("lbj_class", "")
|
lbjClass = jsonData.optString("lbj_class", "")
|
||||||
route = jsonData.optString("route", "")
|
route = jsonData.optString("route", "")
|
||||||
positionInfo = jsonData.optString("position_info", "")
|
positionInfo = jsonData.optString("position_info", "")
|
||||||
rssi = jsonData.optDouble("rssi", 0.0)
|
rssi = jsonData.optDouble("rssi", 0.0)
|
||||||
|
|
||||||
|
|
||||||
_coordinates = null
|
_coordinates = null
|
||||||
|
|
||||||
Log.d(TAG, "Successfully parsed: train=$train, dir=$direction, speed=$speed, lbjClass='$lbjClass'")
|
Log.d(TAG, "Successfully parsed: train=$train, dir=$direction, speed=$speed, lbjClass='$lbjClass', locoType='$locoType'")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "JSON parse error: ${e.message}", e)
|
Log.e(TAG, "JSON parse error: ${e.message}", e)
|
||||||
|
|
||||||
|
|
||||||
try { train = jsonData.optString("train", "") } catch (e: Exception) { }
|
try { train = jsonData.optString("train", "") } catch (e: Exception) { }
|
||||||
try { direction = jsonData.optInt("dir", 0) } catch (e: Exception) { }
|
try { direction = jsonData.optInt("dir", 0) } catch (e: Exception) { }
|
||||||
try { speed = jsonData.optString("speed", "") } catch (e: Exception) { }
|
try { speed = jsonData.optString("speed", "") } catch (e: Exception) { }
|
||||||
@@ -107,7 +121,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_coordinates = LocationUtils.parsePositionInfo(positionInfo)
|
_coordinates = LocationUtil.parsePositionInfo(positionInfo)
|
||||||
return _coordinates
|
return _coordinates
|
||||||
}
|
}
|
||||||
private fun isValidValue(value: String): Boolean {
|
private fun isValidValue(value: String): Boolean {
|
||||||
@@ -134,7 +148,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
|||||||
lbjClass.trim()
|
lbjClass.trim()
|
||||||
} else if (isValidValue(train)) {
|
} else if (isValidValue(train)) {
|
||||||
train.trim()
|
train.trim()
|
||||||
} else ""
|
} else null
|
||||||
|
|
||||||
val map = mutableMapOf<String, String>()
|
val map = mutableMapOf<String, String>()
|
||||||
|
|
||||||
@@ -143,14 +157,17 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
|||||||
map["receivedTimestamp"] = dateFormat.format(receivedTimestamp)
|
map["receivedTimestamp"] = dateFormat.format(receivedTimestamp)
|
||||||
|
|
||||||
|
|
||||||
if (trainDisplay.isNotEmpty()) map["train"] = trainDisplay
|
trainDisplay?.takeIf { it.isNotEmpty() }?.let { map["train"] = it }
|
||||||
|
|
||||||
if (directionText != "未知") map["direction"] = directionText
|
if (directionText != "未知") map["direction"] = directionText
|
||||||
if (isValidValue(speed)) map["speed"] = "速度: ${speed.trim()} km/h"
|
if (isValidValue(speed)) map["speed"] = "${speed.trim()} km/h"
|
||||||
if (isValidValue(position)) map["position"] = "位置: ${position.trim()} km"
|
if (isValidValue(position)) {
|
||||||
|
map["position"] = "${position.trim().removeSuffix(".")} K"
|
||||||
|
}
|
||||||
val timeToDisplay = if (showDetailedTime) {
|
val timeToDisplay = if (showDetailedTime) {
|
||||||
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||||
if (isValidValue(time)) {
|
if (isValidValue(time)) {
|
||||||
"列车时间: $time\n接收时间: ${dateFormat.format(receivedTimestamp)}"
|
"$time\n${dateFormat.format(receivedTimestamp)}"
|
||||||
} else {
|
} else {
|
||||||
dateFormat.format(receivedTimestamp)
|
dateFormat.format(receivedTimestamp)
|
||||||
}
|
}
|
||||||
@@ -164,13 +181,13 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
map["time"] = timeToDisplay
|
map["time"] = timeToDisplay
|
||||||
if (isValidValue(loco)) map["loco"] = "机车号: ${loco.trim()}"
|
if (isValidValue(loco)) map["loco"] = "${loco.trim()}"
|
||||||
if (isValidValue(locoType)) map["loco_type"] = "型号: ${locoType.trim()}"
|
if (isValidValue(locoType)) map["loco_type"] = "${locoType.trim()}"
|
||||||
if (isValidValue(route)) map["route"] = "线路: ${route.trim()}"
|
if (isValidValue(route)) map["route"] = "${route.trim()}"
|
||||||
if (isValidValue(positionInfo) && !positionInfo.trim().matches(Regex(".*(<NUL>|\\s)*.*"))) {
|
if (isValidValue(positionInfo) && !positionInfo.trim().matches(Regex(".*(<NUL>|\\s)*.*"))) {
|
||||||
map["position_info"] = "位置信息: ${positionInfo.trim()}"
|
map["position_info"] = "${positionInfo.trim()}"
|
||||||
}
|
}
|
||||||
if (rssi != 0.0) map["rssi"] = "信号强度: $rssi dBm"
|
if (rssi != 0.0) map["rssi"] = "$rssi dBm"
|
||||||
|
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,12 +297,12 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun processRecordsForMerging(records: List<TrainRecord>, settings: MergeSettings): List<MergedTrainRecord> {
|
private fun processRecordsForMerging(records: List<TrainRecord>, settings: MergeSettings): List<MergedTrainRecord> {
|
||||||
val currentTime = Date()
|
val validRecords = settings.timeWindow.seconds?.let { windowSeconds ->
|
||||||
val validRecords = records.filter { record ->
|
val currentTime = Date()
|
||||||
settings.timeWindow.seconds?.let { windowSeconds ->
|
records.filter { record ->
|
||||||
(currentTime.time - record.timestamp.time) / 1000 <= windowSeconds
|
(currentTime.time - record.timestamp.time) / 1000 <= windowSeconds
|
||||||
} ?: true
|
}
|
||||||
}
|
} ?: records
|
||||||
|
|
||||||
return when (settings.groupBy) {
|
return when (settings.groupBy) {
|
||||||
GroupBy.TRAIN_OR_LOCO -> processTrainOrLocoMerging(validRecords)
|
GroupBy.TRAIN_OR_LOCO -> processTrainOrLocoMerging(validRecords)
|
||||||
@@ -317,11 +317,14 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
|
|
||||||
groupedRecords.mapNotNull { (groupKey, groupRecords) ->
|
groupedRecords.mapNotNull { (groupKey, groupRecords) ->
|
||||||
if (groupRecords.size >= 2) {
|
if (groupRecords.size >= 2) {
|
||||||
val sortedRecords = groupRecords.sortedBy { it.timestamp }
|
val latestRecord = if (groupRecords.size > 1) {
|
||||||
val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!!
|
groupRecords.maxByOrNull { it.timestamp } ?: groupRecords.last()
|
||||||
|
} else {
|
||||||
|
groupRecords.last()
|
||||||
|
}
|
||||||
MergedTrainRecord(
|
MergedTrainRecord(
|
||||||
groupKey = groupKey,
|
groupKey = groupKey,
|
||||||
records = sortedRecords,
|
records = groupRecords.toList(),
|
||||||
latestRecord = latestRecord
|
latestRecord = latestRecord
|
||||||
)
|
)
|
||||||
} else null
|
} else null
|
||||||
@@ -331,7 +334,9 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun processTrainOrLocoMerging(records: List<TrainRecord>): List<MergedTrainRecord> {
|
private fun processTrainOrLocoMerging(records: List<TrainRecord>): List<MergedTrainRecord> {
|
||||||
val groups = mutableListOf<MutableList<TrainRecord>>()
|
val trainGroups = mutableMapOf<String, MutableList<TrainRecord>>()
|
||||||
|
val locoGroups = mutableMapOf<String, MutableList<TrainRecord>>()
|
||||||
|
val mergedGroups = mutableSetOf<MutableList<TrainRecord>>()
|
||||||
|
|
||||||
records.forEach { record ->
|
records.forEach { record ->
|
||||||
val train = record.train.trim()
|
val train = record.train.trim()
|
||||||
@@ -341,38 +346,44 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
var foundGroup: MutableList<TrainRecord>? = null
|
var targetGroup: MutableList<TrainRecord>? = null
|
||||||
|
|
||||||
for (group in groups) {
|
if (train.isNotEmpty() && train != "<NUL>") {
|
||||||
val shouldMerge = group.any { existingRecord ->
|
targetGroup = trainGroups[train]
|
||||||
val existingTrain = existingRecord.train.trim()
|
|
||||||
val existingLoco = existingRecord.loco.trim()
|
|
||||||
|
|
||||||
(train.isNotEmpty() && train != "<NUL>" && train == existingTrain) ||
|
|
||||||
(loco.isNotEmpty() && loco != "<NUL>" && loco == existingLoco)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldMerge) {
|
|
||||||
foundGroup = group
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundGroup != null) {
|
if (targetGroup == null && loco.isNotEmpty() && loco != "<NUL>") {
|
||||||
foundGroup.add(record)
|
targetGroup = locoGroups[loco]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetGroup != null) {
|
||||||
|
targetGroup.add(record)
|
||||||
|
if (train.isNotEmpty() && train != "<NUL>") {
|
||||||
|
trainGroups[train] = targetGroup
|
||||||
|
}
|
||||||
|
if (loco.isNotEmpty() && loco != "<NUL>") {
|
||||||
|
locoGroups[loco] = targetGroup
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
groups.add(mutableListOf(record))
|
val newGroup = mutableListOf(record)
|
||||||
|
mergedGroups.add(newGroup)
|
||||||
|
|
||||||
|
if (train.isNotEmpty() && train != "<NUL>") {
|
||||||
|
trainGroups[train] = newGroup
|
||||||
|
}
|
||||||
|
if (loco.isNotEmpty() && loco != "<NUL>") {
|
||||||
|
locoGroups[loco] = newGroup
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups.mapNotNull { groupRecords ->
|
return mergedGroups.mapNotNull { groupRecords ->
|
||||||
if (groupRecords.size >= 2) {
|
if (groupRecords.size >= 2) {
|
||||||
val sortedRecords = groupRecords.sortedBy { it.timestamp }
|
val latestRecord = groupRecords.maxByOrNull { it.timestamp } ?: groupRecords.lastOrNull() ?: return@mapNotNull null
|
||||||
val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!!
|
|
||||||
val groupKey = "${latestRecord.train}_OR_${latestRecord.loco}"
|
val groupKey = "${latestRecord.train}_OR_${latestRecord.loco}"
|
||||||
MergedTrainRecord(
|
MergedTrainRecord(
|
||||||
groupKey = groupKey,
|
groupKey = groupKey,
|
||||||
records = sortedRecords,
|
records = groupRecords.toList(),
|
||||||
latestRecord = latestRecord
|
latestRecord = latestRecord
|
||||||
)
|
)
|
||||||
} else null
|
} else null
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
package org.noxylva.lbjconsole.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
|
||||||
import org.osmdroid.views.MapView
|
|
||||||
import org.osmdroid.views.overlay.Marker
|
|
||||||
import org.noxylva.lbjconsole.model.TrainRecord
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TrainDetailDialog(
|
|
||||||
trainRecord: TrainRecord,
|
|
||||||
onDismiss: () -> Unit
|
|
||||||
) {
|
|
||||||
val recordMap = trainRecord.toMap()
|
|
||||||
val coordinates = remember { trainRecord.getCoordinates() }
|
|
||||||
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
properties = DialogProperties(
|
|
||||||
dismissOnBackPress = true,
|
|
||||||
dismissOnClickOutside = true
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "列车详情",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
DetailItem("列车号", recordMap["train"] ?: "--")
|
|
||||||
DetailItem("方向", recordMap["direction"] ?: "未知")
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
DetailItem("接收时间", recordMap["timestamp"] ?: "--")
|
|
||||||
DetailItem("列车时间", recordMap["time"] ?: "--")
|
|
||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
DetailItem("速度", recordMap["speed"] ?: "--")
|
|
||||||
DetailItem("位置", recordMap["position"] ?: "--")
|
|
||||||
DetailItem("位置信息", recordMap["position_info"] ?: "--")
|
|
||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
DetailItem("机车号", recordMap["loco"] ?: "--")
|
|
||||||
DetailItem("机车类型", recordMap["loco_type"] ?: "--")
|
|
||||||
DetailItem("列车类型", recordMap["lbj_class"] ?: "--")
|
|
||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
DetailItem("路线", recordMap["route"] ?: "--")
|
|
||||||
DetailItem("信号强度", recordMap["rssi"] ?: "--")
|
|
||||||
|
|
||||||
if (coordinates != null) {
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
DetailItem(
|
|
||||||
label = "经纬度",
|
|
||||||
value = "纬度: ${coordinates.latitude}, 经度: ${coordinates.longitude}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(200.dp)
|
|
||||||
.padding(vertical = 8.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
AndroidView(
|
|
||||||
factory = { context ->
|
|
||||||
MapView(context).apply {
|
|
||||||
setTileSource(TileSourceFactory.MAPNIK)
|
|
||||||
setMultiTouchControls(true)
|
|
||||||
controller.setZoom(15.0)
|
|
||||||
controller.setCenter(coordinates)
|
|
||||||
|
|
||||||
|
|
||||||
val marker = Marker(this)
|
|
||||||
marker.position = coordinates
|
|
||||||
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
|
||||||
marker.title = recordMap["train"] ?: "列车"
|
|
||||||
overlays.add(marker)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update = { mapView ->
|
|
||||||
mapView.controller.setCenter(coordinates)
|
|
||||||
mapView.invalidate()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = onDismiss,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
) {
|
|
||||||
Text("关闭")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DetailItem(
|
|
||||||
label: String,
|
|
||||||
value: String,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = value,
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -155,25 +155,38 @@ fun TrainRecordItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
val hasTrainDisplay = recordMap["train"]?.toString()?.isNotEmpty() ?: false
|
||||||
|
val hasRouteOrPosition = record.route.trim().isNotEmpty() && !record.route.trim().all { it == '*' } ||
|
||||||
|
record.position.trim().isNotEmpty() && !record.position.trim().all { it == '-' || it == '.' } && record.position.trim() != "<NUL>"
|
||||||
|
val hasSpeed = record.speed.trim().isNotEmpty() &&
|
||||||
|
!record.speed.trim().all { it == '*' || it == '-' } &&
|
||||||
|
record.speed.trim() != "NUL" && record.speed.trim() != "<NUL>"
|
||||||
|
val hasLocoInfo = locoInfoUtil != null && record.locoType.isNotEmpty() && record.loco.isNotEmpty() &&
|
||||||
|
locoInfoUtil.getLocoInfoDisplay(record.locoType, record.loco) != null
|
||||||
|
|
||||||
|
val shouldShowOnlyTime = !hasTrainDisplay && !hasRouteOrPosition && !hasSpeed && !hasLocoInfo
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(if (shouldShowOnlyTime) 0.dp else 2.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
val trainDisplay = recordMap["train"]?.toString() ?: "未知列车"
|
val trainDisplay = recordMap["train"]?.toString() ?: ""
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
if (trainDisplay.isNotEmpty()) {
|
||||||
text = trainDisplay,
|
Text(
|
||||||
fontWeight = FontWeight.Bold,
|
text = trainDisplay,
|
||||||
fontSize = 20.sp,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.primary
|
fontSize = 20.sp,
|
||||||
)
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val directionText = when (record.direction) {
|
val directionText = when (record.direction) {
|
||||||
1 -> "下"
|
1 -> "下"
|
||||||
@@ -203,16 +216,16 @@ fun TrainRecordItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val formattedInfo = when {
|
val formattedInfo = when {
|
||||||
record.locoType.isNotEmpty() && record.loco.isNotEmpty() -> {
|
record.locoType.isNotEmpty() && record.loco.isNotEmpty() -> {
|
||||||
val shortLoco = if (record.loco.length > 5) {
|
val shortLoco = if (record.loco.length > 5) {
|
||||||
record.loco.takeLast(5)
|
record.loco.takeLast(5)
|
||||||
} else {
|
} else {
|
||||||
record.loco
|
record.loco
|
||||||
}
|
|
||||||
"${record.locoType}-${shortLoco}"
|
|
||||||
}
|
}
|
||||||
|
"${record.locoType}-${shortLoco}"
|
||||||
|
}
|
||||||
record.locoType.isNotEmpty() -> record.locoType
|
record.locoType.isNotEmpty() -> record.locoType
|
||||||
record.loco.isNotEmpty() -> record.loco
|
record.loco.isNotEmpty() -> record.loco
|
||||||
else -> ""
|
else -> ""
|
||||||
@@ -227,7 +240,7 @@ fun TrainRecordItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(if (shouldShowOnlyTime) 0.dp else 2.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -258,7 +271,7 @@ fun TrainRecordItem(
|
|||||||
|
|
||||||
if (isValidPosition) {
|
if (isValidPosition) {
|
||||||
Text(
|
Text(
|
||||||
text = "${position}K",
|
text = "${position.trim().removeSuffix(".")}K",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
modifier = Modifier.alignByBaseline()
|
modifier = Modifier.alignByBaseline()
|
||||||
@@ -287,7 +300,7 @@ fun TrainRecordItem(
|
|||||||
record.loco
|
record.loco
|
||||||
)
|
)
|
||||||
if (locoInfoText != null) {
|
if (locoInfoText != null) {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(if (shouldShowOnlyTime) 0.dp else 2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = locoInfoText,
|
text = locoInfoText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
@@ -295,7 +308,8 @@ fun TrainRecordItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
Spacer(modifier = Modifier.height(if (shouldShowOnlyTime) 0.dp else 2.dp))
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isExpanded,
|
visible = isExpanded,
|
||||||
enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)),
|
enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)),
|
||||||
@@ -528,18 +542,20 @@ fun MergedTrainRecordItem(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
val trainDisplay = recordMap["train"]?.toString() ?: "未知列车"
|
val trainDisplay = recordMap["train"]?.toString() ?: ""
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
if (trainDisplay.isNotEmpty()) {
|
||||||
text = trainDisplay,
|
Text(
|
||||||
fontWeight = FontWeight.Bold,
|
text = trainDisplay,
|
||||||
fontSize = 20.sp,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.primary
|
fontSize = 20.sp,
|
||||||
)
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val directionText = when (latestRecord.direction) {
|
val directionText = when (latestRecord.direction) {
|
||||||
1 -> "下"
|
1 -> "下"
|
||||||
@@ -572,13 +588,13 @@ fun MergedTrainRecordItem(
|
|||||||
|
|
||||||
val formattedInfo = when {
|
val formattedInfo = when {
|
||||||
latestRecord.locoType.isNotEmpty() && latestRecord.loco.isNotEmpty() -> {
|
latestRecord.locoType.isNotEmpty() && latestRecord.loco.isNotEmpty() -> {
|
||||||
val shortLoco = if (latestRecord.loco.length > 5) {
|
val shortLoco = if (latestRecord.loco.length > 5) {
|
||||||
latestRecord.loco.takeLast(5)
|
latestRecord.loco.takeLast(5)
|
||||||
} else {
|
} else {
|
||||||
latestRecord.loco
|
latestRecord.loco
|
||||||
}
|
|
||||||
"${latestRecord.locoType}-${shortLoco}"
|
|
||||||
}
|
}
|
||||||
|
"${latestRecord.locoType}-${shortLoco}"
|
||||||
|
}
|
||||||
latestRecord.locoType.isNotEmpty() -> latestRecord.locoType
|
latestRecord.locoType.isNotEmpty() -> latestRecord.locoType
|
||||||
latestRecord.loco.isNotEmpty() -> latestRecord.loco
|
latestRecord.loco.isNotEmpty() -> latestRecord.loco
|
||||||
else -> ""
|
else -> ""
|
||||||
@@ -593,7 +609,7 @@ fun MergedTrainRecordItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -624,7 +640,7 @@ fun MergedTrainRecordItem(
|
|||||||
|
|
||||||
if (isValidPosition) {
|
if (isValidPosition) {
|
||||||
Text(
|
Text(
|
||||||
text = "${position}K",
|
text = "${position.trim().removeSuffix(".")}K",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
modifier = Modifier.alignByBaseline()
|
modifier = Modifier.alignByBaseline()
|
||||||
@@ -653,7 +669,7 @@ fun MergedTrainRecordItem(
|
|||||||
latestRecord.loco
|
latestRecord.loco
|
||||||
)
|
)
|
||||||
if (locoInfoText != null) {
|
if (locoInfoText != null) {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = locoInfoText,
|
text = locoInfoText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
@@ -661,7 +677,7 @@ fun MergedTrainRecordItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isExpanded,
|
visible = isExpanded,
|
||||||
enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)),
|
enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)),
|
||||||
@@ -782,14 +798,14 @@ fun MergedTrainRecordItem(
|
|||||||
controller.setZoom(mapViewState.zoom)
|
controller.setZoom(mapViewState.zoom)
|
||||||
controller.setCenter(mapViewState.center)
|
controller.setCenter(mapViewState.center)
|
||||||
} else if (allValidCoordinates.size > 1) {
|
} else if (allValidCoordinates.size > 1) {
|
||||||
val boundingBox = BoundingBox.fromGeoPoints(allValidCoordinates)
|
val boundingBox = BoundingBox.fromGeoPoints(allValidCoordinates.filter { it.latitude != 0.0 || it.longitude != 0.0 })
|
||||||
val layoutListener = object : android.view.View.OnLayoutChangeListener {
|
val layoutListener = object : android.view.View.OnLayoutChangeListener {
|
||||||
override fun onLayoutChange(v: android.view.View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
override fun onLayoutChange(v: android.view.View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
||||||
if (width > 0 && height > 0) {
|
if (width > 0 && height > 0) {
|
||||||
val zoomLevel = org.osmdroid.views.MapView.getTileSystem().getBoundingBoxZoom(boundingBox, width, height)
|
val zoomLevel = org.osmdroid.views.MapView.getTileSystem().getBoundingBoxZoom(boundingBox, width, height)
|
||||||
val latSpan = boundingBox.latitudeSpan
|
val latSpan = boundingBox.latitudeSpan
|
||||||
val adjustedCenter = org.osmdroid.util.GeoPoint(
|
val adjustedCenter = org.osmdroid.util.GeoPoint(
|
||||||
boundingBox.center.latitude + latSpan * 0.25, // Shift center UP (north) to create top padding
|
boundingBox.center.latitude + latSpan * 0.25,
|
||||||
boundingBox.center.longitude
|
boundingBox.center.longitude
|
||||||
)
|
)
|
||||||
val newZoom = zoomLevel - 1.0
|
val newZoom = zoomLevel - 1.0
|
||||||
@@ -805,7 +821,7 @@ fun MergedTrainRecordItem(
|
|||||||
addOnLayoutChangeListener(layoutListener)
|
addOnLayoutChangeListener(layoutListener)
|
||||||
} else if (allValidCoordinates.isNotEmpty()) {
|
} else if (allValidCoordinates.isNotEmpty()) {
|
||||||
val center = allValidCoordinates.first()
|
val center = allValidCoordinates.first()
|
||||||
val zoom = 14.0
|
val zoom = 10.0
|
||||||
controller.setZoom(zoom)
|
controller.setZoom(zoom)
|
||||||
controller.setCenter(center)
|
controller.setCenter(center)
|
||||||
onMapViewStateChange(CardMapView(center, zoom))
|
onMapViewStateChange(CardMapView(center, zoom))
|
||||||
@@ -907,12 +923,12 @@ fun MergedTrainRecordItem(
|
|||||||
}
|
}
|
||||||
if (recordItem.position.isNotEmpty() && recordItem.position != "<NUL>") {
|
if (recordItem.position.isNotEmpty() && recordItem.position != "<NUL>") {
|
||||||
if (isNotEmpty()) append(" ")
|
if (isNotEmpty()) append(" ")
|
||||||
append("${recordItem.position}K")
|
append("${recordItem.position.trim().removeSuffix(".")}K")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = locationText.ifEmpty { "位置未知" },
|
text = locationText.ifEmpty { "" },
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ import android.graphics.PorterDuff
|
|||||||
import android.graphics.PorterDuffColorFilter
|
import android.graphics.PorterDuffColorFilter
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.MyLocation
|
import androidx.compose.material.icons.filled.MyLocation
|
||||||
@@ -16,11 +21,16 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -112,7 +122,6 @@ fun MapScreen(
|
|||||||
|
|
||||||
val recordMap = record.toMap()
|
val recordMap = record.toMap()
|
||||||
title = recordMap["train"]?.toString() ?: "列车"
|
title = recordMap["train"]?.toString() ?: "列车"
|
||||||
|
|
||||||
val latStr = String.format("%.4f", point.latitude)
|
val latStr = String.format("%.4f", point.latitude)
|
||||||
val lonStr = String.format("%.4f", point.longitude)
|
val lonStr = String.format("%.4f", point.longitude)
|
||||||
val coordStr = "${latStr}°N, ${lonStr}°E"
|
val coordStr = "${latStr}°N, ${lonStr}°E"
|
||||||
@@ -574,8 +583,8 @@ fun Context.getCompactMarkerDrawable(color: Int): Drawable {
|
|||||||
|
|
||||||
|
|
||||||
private fun Int.directionText(): String = when (this) {
|
private fun Int.directionText(): String = when (this) {
|
||||||
1 -> "↓"
|
1 -> "下行"
|
||||||
3 -> "↑"
|
3 -> "上行"
|
||||||
else -> "?"
|
else -> "?"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,50 +594,143 @@ private fun TrainMarkerDialog(
|
|||||||
position: GeoPoint?,
|
position: GeoPoint?,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val recordMap = record.toMap()
|
||||||
|
|
||||||
|
val displayItems = recordMap.filterKeys {
|
||||||
|
it !in setOf("train", "direction", "time")
|
||||||
|
}.toList()
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = {
|
title = {
|
||||||
|
|
||||||
val recordMap = record.toMap()
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(text = recordMap["train"]?.toString() ?: "列车", style = MaterialTheme.typography.titleLarge)
|
Text(
|
||||||
|
text = recordMap["train"]?.toString() ?: "列车",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
recordMap["direction"]?.let { direction ->
|
recordMap["direction"]?.let { direction ->
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = direction,
|
text = (direction as? Int)?.directionText() ?: direction.toString(),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontSize = 12.sp
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
record.toMap().forEach { (key, value) ->
|
.fillMaxWidth()
|
||||||
if (key != "train" && key != "direction") {
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
displayItems.forEach { (key, value) ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
val title = when (key) {
|
||||||
|
"speed" -> "速度"
|
||||||
|
"position" -> "位置"
|
||||||
|
"time" -> "时间"
|
||||||
|
"loco" -> "机车号"
|
||||||
|
"loco_type" -> "机车型号"
|
||||||
|
"route" -> "线路"
|
||||||
|
"rssi" -> "信号强度"
|
||||||
|
"timestamp" -> "时间"
|
||||||
|
"receivedTimestamp" -> "接收时间"
|
||||||
|
else -> key
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = value,
|
text = title,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.titleMedium
|
||||||
modifier = Modifier.padding(vertical = 2.dp)
|
)
|
||||||
|
Text(
|
||||||
|
text = value.toString(),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
position?.let {
|
position?.let {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Row(
|
||||||
Text(
|
modifier = Modifier
|
||||||
text = "坐标: ${String.format("%.6f", it.latitude)}, ${String.format("%.6f", it.longitude)}",
|
.fillMaxWidth()
|
||||||
style = MaterialTheme.typography.bodyMedium
|
.padding(vertical = 4.dp),
|
||||||
)
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "坐标信息",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${String.format("%.6f", it.latitude)}, ${String.format("%.6f", it.longitude)}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(onClick = onDismiss) {
|
||||||
Text("确定")
|
Text("关闭")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoSection(title: String, items: List<Pair<String, String>>) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
|
)
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
items.forEach { (key, value) ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 2.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = value.toString(),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoSectionSimple(title: String, items: List<Pair<String, String>>) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
items.forEach { (key, value) ->
|
||||||
|
Text(
|
||||||
|
text = value.toString(),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
package org.noxylva.lbjconsole.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.animation.core.*
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import org.noxylva.lbjconsole.model.TrainRecord
|
|
||||||
import org.noxylva.lbjconsole.ui.components.TrainDetailDialog
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MonitorScreen(
|
|
||||||
latestRecord: TrainRecord?,
|
|
||||||
recentRecords: List<TrainRecord>,
|
|
||||||
lastUpdateTime: Date?,
|
|
||||||
temporaryStatusMessage: String? = null,
|
|
||||||
onRecordClick: (TrainRecord) -> Unit,
|
|
||||||
onClearLog: () -> Unit
|
|
||||||
) {
|
|
||||||
var showDetailDialog by remember { mutableStateOf(false) }
|
|
||||||
var selectedRecord by remember { mutableStateOf<TrainRecord?>(null) }
|
|
||||||
var isPressed by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val scale by animateFloatAsState(
|
|
||||||
targetValue = if (isPressed) 0.98f else 1f,
|
|
||||||
animationSpec = tween(durationMillis = 120),
|
|
||||||
label = "content_scale"
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(isPressed) {
|
|
||||||
if (isPressed) {
|
|
||||||
delay(100)
|
|
||||||
isPressed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val timeSinceLastUpdate = remember { mutableStateOf<String?>(null) }
|
|
||||||
LaunchedEffect(key1 = lastUpdateTime) {
|
|
||||||
if (lastUpdateTime != null) {
|
|
||||||
while (true) {
|
|
||||||
val now = Date()
|
|
||||||
val diffInSec = (now.time - lastUpdateTime.time) / 1000
|
|
||||||
timeSinceLastUpdate.value = when {
|
|
||||||
diffInSec < 60 -> "${diffInSec}秒前"
|
|
||||||
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
|
|
||||||
else -> "${diffInSec / 3600}小时前"
|
|
||||||
}
|
|
||||||
val updateInterval = if (diffInSec < 60) 500L else if (diffInSec < 3600) 30000L else 300000L
|
|
||||||
delay(updateInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
|
||||||
Card(modifier = Modifier.fillMaxSize()) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(20.dp)
|
|
||||||
) {
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = timeSinceLastUpdate.value ?: "暂无数据",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = latestRecord,
|
|
||||||
transitionSpec = {
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 300,
|
|
||||||
easing = FastOutSlowInEasing
|
|
||||||
)
|
|
||||||
) + slideInVertically(
|
|
||||||
initialOffsetY = { it / 4 },
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 300,
|
|
||||||
easing = FastOutSlowInEasing
|
|
||||||
)
|
|
||||||
) togetherWith fadeOut(
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 150,
|
|
||||||
easing = FastOutLinearInEasing
|
|
||||||
)
|
|
||||||
) + slideOutVertically(
|
|
||||||
targetOffsetY = { -it / 4 },
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 150,
|
|
||||||
easing = FastOutLinearInEasing
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = "content_animation"
|
|
||||||
) { record ->
|
|
||||||
if (record != null) {
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = rememberRipple(bounded = true)
|
|
||||||
) {
|
|
||||||
isPressed = true
|
|
||||||
selectedRecord = record
|
|
||||||
showDetailDialog = true
|
|
||||||
onRecordClick(record)
|
|
||||||
}
|
|
||||||
.padding(8.dp)
|
|
||||||
.graphicsLayer {
|
|
||||||
scaleX = scale
|
|
||||||
scaleY = scale
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
|
|
||||||
val recordMap = record.toMap()
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = recordMap["train"]?.toString() ?: "",
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = recordMap["direction"]?.toString() ?: "",
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = when(recordMap["direction"]?.toString()) {
|
|
||||||
"上行" -> MaterialTheme.colorScheme.primary
|
|
||||||
"下行" -> MaterialTheme.colorScheme.secondary
|
|
||||||
else -> MaterialTheme.colorScheme.onSurface
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
|
|
||||||
|
|
||||||
if (recordMap.containsKey("time")) {
|
|
||||||
recordMap["time"]?.split("\n")?.forEach { timeLine ->
|
|
||||||
Text(
|
|
||||||
text = timeLine,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(thickness = 0.5.dp)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
recordMap["speed"]?.let { speed ->
|
|
||||||
Text(
|
|
||||||
text = speed,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
recordMap["position"]?.let { position ->
|
|
||||||
Text(
|
|
||||||
text = position,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
recordMap.forEach { (key, value) ->
|
|
||||||
when (key) {
|
|
||||||
"timestamp", "train", "direction", "time", "speed", "position", "position_info" -> {}
|
|
||||||
else -> {
|
|
||||||
Text(
|
|
||||||
text = value,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (recordMap.containsKey("position_info")) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = recordMap["position_info"] ?: "",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text(
|
|
||||||
"暂无列车信息",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
color = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
|
|
||||||
if (lastUpdateTime != null) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
"上次接收数据: ${SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(lastUpdateTime)}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (showDetailDialog && selectedRecord != null) {
|
|
||||||
TrainDetailDialog(
|
|
||||||
trainRecord = selectedRecord!!,
|
|
||||||
onDismiss = { showDetailDialog = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun InfoItem(
|
|
||||||
label: String,
|
|
||||||
value: String,
|
|
||||||
fontSize: TextUnit = 14.sp
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 2.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "$label: ",
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
fontSize = fontSize,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = value,
|
|
||||||
fontSize = fontSize,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,8 @@ import android.util.Log
|
|||||||
import org.osmdroid.util.GeoPoint
|
import org.osmdroid.util.GeoPoint
|
||||||
|
|
||||||
|
|
||||||
object LocationUtils {
|
object LocationUtil {
|
||||||
private const val TAG = "LocationUtils"
|
private const val TAG = "LocationUtil"
|
||||||
|
|
||||||
|
|
||||||
fun parsePositionInfo(positionInfo: String): GeoPoint? {
|
fun parsePositionInfo(positionInfo: String): GeoPoint? {
|
||||||
@@ -52,7 +52,7 @@ object LocationUtils {
|
|||||||
|
|
||||||
val minuteEndIndex = dmsString.indexOf('′')
|
val minuteEndIndex = dmsString.indexOf('′')
|
||||||
if (minuteEndIndex == -1) {
|
if (minuteEndIndex == -1) {
|
||||||
return degrees
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val minutes = dmsString.substring(degreeIndex + 1, minuteEndIndex).toDouble()
|
val minutes = dmsString.substring(degreeIndex + 1, minuteEndIndex).toDouble()
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.noxylva.lbjconsole.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
|
class LocoTypeUtil(private val context: Context) {
|
||||||
|
private val locoTypeMap = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadLocoTypeMapping()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadLocoTypeMapping() {
|
||||||
|
try {
|
||||||
|
context.assets.open("loco_type_info.csv").use { inputStream ->
|
||||||
|
BufferedReader(InputStreamReader(inputStream)).use { reader ->
|
||||||
|
reader.lines().forEach { line ->
|
||||||
|
val parts = line.split(",")
|
||||||
|
if (parts.size >= 2) {
|
||||||
|
val code = parts[0].trim()
|
||||||
|
val type = parts[1].trim()
|
||||||
|
locoTypeMap[code] = type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocoTypeByCode(code: String): String? {
|
||||||
|
return locoTypeMap[code]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocoTypeByLocoNumber(locoNumber: String): String? {
|
||||||
|
if (locoNumber.length < 3) return null
|
||||||
|
val prefix = locoNumber.take(3)
|
||||||
|
return getLocoTypeByCode(prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user