feat: update locomotive type support and optimized position display format
This commit is contained in:
@@ -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`:包含车次类型信息,格式为 `正则表达式,车次类型`。
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(",")
|
||||
|
||||
Reference in New Issue
Block a user