better Public transport vehicle map

This commit is contained in:
2026-09-06 13:15:28 +02:00
parent a7f2be8a3e
commit 7ad24463b7
@@ -1,29 +1,37 @@
package org.pupes.mhdrunpathfinder
import android.app.Activity
import android.graphics.drawable.Icon
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.transit.realtime.GtfsRealtime
import kotlinx.serialization.Serializable
import okhttp3.OkHttpClient
import okhttp3.Request
import ovh.plrapps.mapcompose.api.addClusterer
import ovh.plrapps.mapcompose.api.addMarker
import ovh.plrapps.mapcompose.api.hasMarker
import ovh.plrapps.mapcompose.api.moveMarker
import ovh.plrapps.mapcompose.api.removeMarker
import ovh.plrapps.mapcompose.api.visibleArea
import ovh.plrapps.mapcompose.ui.state.markers.model.RenderingStrategy
import java.net.URL
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.mutableStateMapOf
@Serializable
data class GTFSRoute(
@@ -88,6 +96,33 @@ val gtfshttpClient = OkHttpClient.Builder()
private val GTFSMainHandler = Handler(Looper.getMainLooper())
private var GTFSTrackingThread: Thread? = null
private var GTFSIsTracking = false
private val activeVehicleIds = mutableSetOf<String>()
private data class VehicleUiState(val bearing: Float, val routeShortName: String?)
private val vehicleUiStates = mutableStateMapOf<String, VehicleUiState>()
private const val GTFS_CLUSTERER_ID = "gtfs_vehicles"
private var gtfsClustererInitialized = false
private fun ensureGtfsClusterer() {
if (gtfsClustererInitialized) return
mapState.addClusterer(GTFS_CLUSTERER_ID, 30.dp) { ids ->
{
Surface(
shape = MaterialTheme.shapes.small,
color = Color(0xFF31FF00)
) {
Text(
text = "${ids.size}",
modifier = Modifier.padding(4.dp),
fontSize = 11.sp,
color = Color.Black
)
}
}
}
gtfsClustererInitialized = true
}
private fun fetchGTFSPositions(gtfsUrl: URL, api_key: String, callee: Activity) {
try {
@@ -102,35 +137,84 @@ private fun fetchGTFSPositions(gtfsUrl: URL, api_key: String, callee: Activity)
throw Exception("Error: ${response.code} ${response.message}")
}
val bytes = response.body?.bytes() ?: throw Exception("Empty response")
val feed = GtfsRealtime.FeedMessage.parseFrom(bytes)
val feed = response.body?.byteStream()?.use { stream ->
GtfsRealtime.FeedMessage.parseFrom(stream)
} ?: throw Exception("Empty response")
data class VehicleUpdate(
val id: String,
val x: Double,
val y: Double,
val bearing: Float,
val routeShortName: String?
)
val seenIds = mutableSetOf<String>()
val updates = mutableListOf<VehicleUpdate>()
feed.entityList.forEach { entity ->
if (entity.hasVehicle()) {
GTFSMainHandler.post {
if(mapState.hasMarker(entity.vehicle.vehicle.id)) {
mapState.moveMarker(entity.vehicle.vehicle.id, x = longitudeToXNormalized(entity.vehicle.position.longitude.toDouble()), y = latitudeToYNormalized(entity.vehicle.position.latitude.toDouble()));
} else {
mapState.addMarker(
entity.vehicle.vehicle.id,
val id = entity.vehicle.vehicle.id
seenIds.add(id)
val routeId = entity.vehicle.trip.routeId
updates.add(
VehicleUpdate(
id = id,
x = longitudeToXNormalized(entity.vehicle.position.longitude.toDouble()),
y = latitudeToYNormalized(entity.vehicle.position.latitude.toDouble()),
Offset(-0.5f, -0.5f)
bearing = entity.vehicle.position.bearing,
routeShortName = gtfsRoutes.find { it.route_id == routeId }?.route_short_name
)
)
}
}
GTFSMainHandler.post {
ensureGtfsClusterer()
updates.forEach { u ->
vehicleUiStates[u.id] = VehicleUiState(u.bearing, u.routeShortName)
if (mapState.hasMarker(u.id)) {
mapState.moveMarker(u.id, x = u.x, y = u.y)
} else {
mapState.addMarker(
u.id,
x = u.x,
y = u.y,
Offset(-0.5f, -0.5f),
renderingStrategy = RenderingStrategy.Clustering(GTFS_CLUSTERER_ID)
) {
val state = vehicleUiStates[u.id]
Box(
modifier = Modifier.size(28.dp),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = R.drawable.mhd_icon),
contentDescription = null,
modifier = Modifier.size(14.dp).rotate(entity.vehicle.position.bearing),
modifier = Modifier.size(16.dp).rotate(state?.bearing ?: u.bearing),
tint = Color(0xFF31FF00)
)
// Text("${(gtfsRoutes.find {it.route_id == entity.tripUpdate.trip.routeId})?.route_short_name}")
Text(
"${state?.routeShortName ?: u.routeShortName}",
color = Color.Red,
fontSize = 10.sp,
modifier = Modifier.align(Alignment.Center)
)
}
}
activeVehicleIds.add(u.id)
}
// use vehicle
}
val stale = activeVehicleIds - seenIds
stale.forEach { staleId ->
mapState.removeMarker(staleId)
vehicleUiStates.remove(staleId)
}
activeVehicleIds.removeAll(stale)
activeVehicleIds.addAll(seenIds)
}
}
} catch (e: Exception) {
@@ -248,4 +332,7 @@ fun stopGTFSTracking() {
}
GTFSTrackingThread?.interrupt()
GTFSTrackingThread = null
activeVehicleIds.clear()
vehicleUiStates.clear()
gtfsClustererInitialized = false
}