feat: update locomotive type support and optimized position display format

This commit is contained in:
Nedifinita
2025-08-19 19:10:04 +08:00
parent 0f98b6bcf7
commit 0bf7033c6c
9 changed files with 147 additions and 215 deletions

View File

@@ -3,14 +3,15 @@
LBJ Console 是一款 Android 应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) 设备接收并显示列车预警消息,功能包括:
- 接收列车预警消息,支持可选的手机推送通知。
- 显示预警消息的 GPS 信息于地图
- 基于内置数据文件显示机车配属和车次类型。
- 在地图上显示预警消息的 GPS 信息。
- 基于内置数据文件显示机车配属,机车类型和车次类型。
## 数据文件
LBJ Console 依赖以下数据文件,位于 `app/src/main/assets/` 目录,用于支持机车配属和车次信息的展示:
- `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注`
- `loco_type_info.csv`:包含机车类型编码信息,格式为 `机车类型编码,机车类型`
- `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型`

View File

@@ -13,8 +13,8 @@ android {
applicationId = "org.noxylva.lbjconsole"
minSdk = 29
targetSdk = 35
versionCode = 11
versionName = "0.1.2"
versionCode = 13
versionName = "0.1.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -145,7 +145,7 @@ class NotificationService(private val context: Context) {
}
if (isValidValue(trainRecord.position)) {
remoteViews.setTextViewText(R.id.notification_position, "${trainRecord.position.trim()}K")
remoteViews.setTextViewText(R.id.notification_position, "${trainRecord.position.trim().removeSuffix(".")}K")
remoteViews.setViewVisibility(R.id.notification_position, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.notification_position, View.GONE)

View File

@@ -85,7 +85,6 @@ class TrainRecord(jsonData: JSONObject? = null) {
time = jsonData.optString("time", "")
loco = jsonData.optString("loco", "")
// 不再直接从JSON获取loco_type而是从loco字段前三位获取
locoType = if (loco.isNotEmpty()) {
val prefix = if (loco.length >= 3) loco.take(3) else loco
LocoTypeUtil?.getLocoTypeByCode(prefix) ?: ""
@@ -161,12 +160,14 @@ class TrainRecord(jsonData: JSONObject? = null) {
trainDisplay?.takeIf { it.isNotEmpty() }?.let { map["train"] = it }
if (directionText != "未知") map["direction"] = directionText
if (isValidValue(speed)) map["speed"] = "速度: ${speed.trim()} km/h"
if (isValidValue(position)) map["position"] = "位置: ${position.trim()} km"
if (isValidValue(speed)) map["speed"] = "${speed.trim()} km/h"
if (isValidValue(position)) {
map["position"] = "${position.trim().removeSuffix(".")} K"
}
val timeToDisplay = if (showDetailedTime) {
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
if (isValidValue(time)) {
"列车时间: $time\n接收时间: ${dateFormat.format(receivedTimestamp)}"
"$time\n${dateFormat.format(receivedTimestamp)}"
} else {
dateFormat.format(receivedTimestamp)
}
@@ -180,13 +181,13 @@ class TrainRecord(jsonData: JSONObject? = null) {
}
}
map["time"] = timeToDisplay
if (isValidValue(loco)) map["loco"] = "机车号: ${loco.trim()}"
if (isValidValue(locoType)) map["loco_type"] = "型号: ${locoType.trim()}"
if (isValidValue(route)) map["route"] = "线路: ${route.trim()}"
if (isValidValue(loco)) map["loco"] = "${loco.trim()}"
if (isValidValue(locoType)) map["loco_type"] = "${locoType.trim()}"
if (isValidValue(route)) map["route"] = "${route.trim()}"
if (isValidValue(positionInfo) && !positionInfo.trim().matches(Regex(".*(<NUL>|\\s)*.*"))) {
map["position_info"] = "位置信息: ${positionInfo.trim()}"
map["position_info"] = "${positionInfo.trim()}"
}
if (rssi != 0.0) map["rssi"] = "信号强度: $rssi dBm"
if (rssi != 0.0) map["rssi"] = "$rssi dBm"
return map
}

View File

@@ -1,172 +0,0 @@
package org.noxylva.lbjconsole.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import org.noxylva.lbjconsole.model.TrainRecord
@Composable
fun TrainDetailDialog(
trainRecord: TrainRecord,
onDismiss: () -> Unit
) {
val recordMap = trainRecord.toMap()
val coordinates = remember { trainRecord.getCoordinates() }
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Text(
text = "列车详情",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
DetailItem("列车号", recordMap["train"] ?: "--")
DetailItem("方向", recordMap["direction"] ?: "未知")
}
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
DetailItem("接收时间", recordMap["timestamp"] ?: "--")
DetailItem("列车时间", recordMap["time"] ?: "--")
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
DetailItem("速度", recordMap["speed"] ?: "--")
DetailItem("位置", recordMap["position"] ?: "--")
DetailItem("位置信息", recordMap["position_info"] ?: "--")
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
DetailItem("机车号", recordMap["loco"] ?: "--")
DetailItem("机车类型", recordMap["loco_type"] ?: "--")
DetailItem("列车类型", recordMap["lbj_class"] ?: "--")
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
DetailItem("路线", recordMap["route"] ?: "--")
DetailItem("信号强度", recordMap["rssi"] ?: "--")
if (coordinates != null) {
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
DetailItem(
label = "经纬度",
value = "纬度: ${coordinates.latitude}, 经度: ${coordinates.longitude}"
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(vertical = 8.dp),
contentAlignment = Alignment.Center
) {
AndroidView(
factory = { context ->
MapView(context).apply {
setTileSource(TileSourceFactory.MAPNIK)
setMultiTouchControls(true)
controller.setZoom(15.0)
controller.setCenter(coordinates)
val marker = Marker(this)
marker.position = coordinates
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
marker.title = recordMap["train"] ?: "列车"
overlays.add(marker)
}
},
update = { mapView ->
mapView.controller.setCenter(coordinates)
mapView.invalidate()
}
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onDismiss,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
) {
Text("关闭")
}
}
}
}
}
@Composable
private fun DetailItem(
label: String,
value: String,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = value,
style = MaterialTheme.typography.bodyLarge
)
}
}

View File

@@ -271,7 +271,7 @@ fun TrainRecordItem(
if (isValidPosition) {
Text(
text = "${position}K",
text = "${position.trim().removeSuffix(".")}K",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.alignByBaseline()
@@ -640,7 +640,7 @@ fun MergedTrainRecordItem(
if (isValidPosition) {
Text(
text = "${position}K",
text = "${position.trim().removeSuffix(".")}K",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.alignByBaseline()
@@ -923,12 +923,12 @@ fun MergedTrainRecordItem(
}
if (recordItem.position.isNotEmpty() && recordItem.position != "<NUL>") {
if (isNotEmpty()) append(" ")
append("${recordItem.position}K")
append("${recordItem.position.trim().removeSuffix(".")}K")
}
}
Text(
text = locationText.ifEmpty { "位置未知" },
text = locationText.ifEmpty { "" },
fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)

View File

@@ -7,7 +7,12 @@ import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.util.Log
import android.view.ViewGroup
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.MyLocation
@@ -16,11 +21,16 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import kotlinx.coroutines.launch
@@ -112,7 +122,6 @@ fun MapScreen(
val recordMap = record.toMap()
title = recordMap["train"]?.toString() ?: "列车"
val latStr = String.format("%.4f", point.latitude)
val lonStr = String.format("%.4f", point.longitude)
val coordStr = "${latStr}°N, ${lonStr}°E"
@@ -574,8 +583,8 @@ fun Context.getCompactMarkerDrawable(color: Int): Drawable {
private fun Int.directionText(): String = when (this) {
1 -> ""
3 -> ""
1 -> "下行"
3 -> "上行"
else -> "?"
}
@@ -585,50 +594,143 @@ private fun TrainMarkerDialog(
position: GeoPoint?,
onDismiss: () -> Unit
) {
val recordMap = record.toMap()
val displayItems = recordMap.filterKeys {
it !in setOf("train", "direction", "time")
}.toList()
AlertDialog(
onDismissRequest = onDismiss,
title = {
val recordMap = record.toMap()
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = recordMap["train"]?.toString() ?: "列车", style = MaterialTheme.typography.titleLarge)
Text(
text = recordMap["train"]?.toString() ?: "列车",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.weight(1f)
)
recordMap["direction"]?.let { direction ->
Spacer(modifier = Modifier.width(8.dp))
Text(
text = direction,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
text = (direction as? Int)?.directionText() ?: direction.toString(),
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp
),
modifier = Modifier.padding(start = 8.dp)
)
}
}
},
text = {
Column {
record.toMap().forEach { (key, value) ->
if (key != "train" && key != "direction") {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(vertical = 8.dp)
) {
displayItems.forEach { (key, value) ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
val title = when (key) {
"speed" -> "速度"
"position" -> "位置"
"time" -> "时间"
"loco" -> "机车号"
"loco_type" -> "机车型号"
"route" -> "线路"
"rssi" -> "信号强度"
"timestamp" -> "时间"
"receivedTimestamp" -> "接收时间"
else -> key
}
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 2.dp)
text = title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = value.toString(),
style = MaterialTheme.typography.bodyMedium
)
}
}
position?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "坐标: ${String.format("%.6f", it.latitude)}, ${String.format("%.6f", it.longitude)}",
style = MaterialTheme.typography.bodyMedium
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "坐标信息",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "${String.format("%.6f", it.latitude)}, ${String.format("%.6f", it.longitude)}",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text("确定")
Text("关闭")
}
}
)
}
@Composable
private fun InfoSection(title: String, items: List<Pair<String, String>>) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 4.dp)
)
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
),
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(12.dp)) {
items.forEach { (key, value) ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = value.toString(),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
}
@Composable
private fun InfoSectionSimple(title: String, items: List<Pair<String, String>>) {
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
items.forEach { (key, value) ->
Text(
text = value.toString(),
style = MaterialTheme.typography.bodyMedium
)
}
}
}

View File

@@ -13,7 +13,7 @@ class LocoTypeUtil(private val context: Context) {
private fun loadLocoTypeMapping() {
try {
context.assets.open("loco_number_info.csv").use { inputStream ->
context.assets.open("loco_type_info.csv").use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader ->
reader.lines().forEach { line ->
val parts = line.split(",")