diff --git a/README.md b/README.md index a215e7f..0f8f23c 100644 --- a/README.md +++ b/README.md @@ -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`:包含车次类型信息,格式为 `正则表达式,车次类型`。 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6001726..75e3f70 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" } diff --git a/app/src/main/assets/loco_number_info.csv b/app/src/main/assets/loco_type_info.csv similarity index 100% rename from app/src/main/assets/loco_number_info.csv rename to app/src/main/assets/loco_type_info.csv diff --git a/app/src/main/java/org/noxylva/lbjconsole/NotificationService.kt b/app/src/main/java/org/noxylva/lbjconsole/NotificationService.kt index 363188f..d0cb537 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/NotificationService.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/NotificationService.kt @@ -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) diff --git a/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecord.kt b/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecord.kt index a6c52a9..57d7686 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecord.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecord.kt @@ -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(".*(|\\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 } diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainDetailDialog.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainDetailDialog.kt deleted file mode 100644 index 62c0eb5..0000000 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainDetailDialog.kt +++ /dev/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 - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt index 79b0b1c..d0fa51d 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt @@ -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 != "") { 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 ) diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MapScreen.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MapScreen.kt index 8e5e867..faadac8 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MapScreen.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MapScreen.kt @@ -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>) { + 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>) { + 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 + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/noxylva/lbjconsole/util/LocoTypeUtil.kt b/app/src/main/java/org/noxylva/lbjconsole/util/LocoTypeUtil.kt index 5929686..50c5a4e 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/util/LocoTypeUtil.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/util/LocoTypeUtil.kt @@ -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(",")