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 package org.pupes.mhdrunpathfinder
import android.app.Activity import android.app.Activity
import android.graphics.drawable.Icon
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.transit.realtime.GtfsRealtime import com.google.transit.realtime.GtfsRealtime
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import ovh.plrapps.mapcompose.api.addClusterer
import ovh.plrapps.mapcompose.api.addMarker import ovh.plrapps.mapcompose.api.addMarker
import ovh.plrapps.mapcompose.api.hasMarker import ovh.plrapps.mapcompose.api.hasMarker
import ovh.plrapps.mapcompose.api.moveMarker import ovh.plrapps.mapcompose.api.moveMarker
import ovh.plrapps.mapcompose.api.removeMarker 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 java.net.URL
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.mutableStateMapOf
@Serializable @Serializable
data class GTFSRoute( data class GTFSRoute(
@@ -88,6 +96,33 @@ val gtfshttpClient = OkHttpClient.Builder()
private val GTFSMainHandler = Handler(Looper.getMainLooper()) private val GTFSMainHandler = Handler(Looper.getMainLooper())
private var GTFSTrackingThread: Thread? = null private var GTFSTrackingThread: Thread? = null
private var GTFSIsTracking = false 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) { private fun fetchGTFSPositions(gtfsUrl: URL, api_key: String, callee: Activity) {
try { try {
@@ -102,36 +137,85 @@ private fun fetchGTFSPositions(gtfsUrl: URL, api_key: String, callee: Activity)
throw Exception("Error: ${response.code} ${response.message}") throw Exception("Error: ${response.code} ${response.message}")
} }
val bytes = response.body?.bytes() ?: throw Exception("Empty response") val feed = response.body?.byteStream()?.use { stream ->
val feed = GtfsRealtime.FeedMessage.parseFrom(bytes) 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 -> feed.entityList.forEach { entity ->
if (entity.hasVehicle()) { if (entity.hasVehicle()) {
GTFSMainHandler.post { val id = entity.vehicle.vehicle.id
seenIds.add(id)
if(mapState.hasMarker(entity.vehicle.vehicle.id)) { val routeId = entity.vehicle.trip.routeId
mapState.moveMarker(entity.vehicle.vehicle.id, x = longitudeToXNormalized(entity.vehicle.position.longitude.toDouble()), y = latitudeToYNormalized(entity.vehicle.position.latitude.toDouble())); updates.add(
VehicleUpdate(
} else { id = id,
mapState.addMarker(
entity.vehicle.vehicle.id,
x = longitudeToXNormalized(entity.vehicle.position.longitude.toDouble()), x = longitudeToXNormalized(entity.vehicle.position.longitude.toDouble()),
y = latitudeToYNormalized(entity.vehicle.position.latitude.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
Icon( )
painter = painterResource(id = R.drawable.mhd_icon), )
contentDescription = null,
modifier = Modifier.size(14.dp).rotate(entity.vehicle.position.bearing),
tint = Color(0xFF31FF00)
)
// Text("${(gtfsRoutes.find {it.route_id == entity.tripUpdate.trip.routeId})?.route_short_name}")
}
}
}
// use vehicle
} }
} }
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(16.dp).rotate(state?.bearing ?: u.bearing),
tint = Color(0xFF31FF00)
)
Text(
"${state?.routeShortName ?: u.routeShortName}",
color = Color.Red,
fontSize = 10.sp,
modifier = Modifier.align(Alignment.Center)
)
}
}
activeVehicleIds.add(u.id)
}
}
val stale = activeVehicleIds - seenIds
stale.forEach { staleId ->
mapState.removeMarker(staleId)
vehicleUiStates.remove(staleId)
}
activeVehicleIds.removeAll(stale)
activeVehicleIds.addAll(seenIds)
}
} }
} catch (e: Exception) { } catch (e: Exception) {
GTFSMainHandler.post { GTFSMainHandler.post {
@@ -248,4 +332,7 @@ fun stopGTFSTracking() {
} }
GTFSTrackingThread?.interrupt() GTFSTrackingThread?.interrupt()
GTFSTrackingThread = null GTFSTrackingThread = null
activeVehicleIds.clear()
vehicleUiStates.clear()
gtfsClustererInitialized = false
} }