From 7ad24463b76cab9f11f822ebc66439e44b957c6f Mon Sep 17 00:00:00 2001 From: PoliEcho Date: Sun, 6 Sep 2026 13:15:28 +0200 Subject: [PATCH] better Public transport vehicle map --- .../org/pupes/mhdrunpathfinder/GTFSUtils.kt | 137 ++++++++++++++---- 1 file changed, 112 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/pupes/mhdrunpathfinder/GTFSUtils.kt b/app/src/main/java/org/pupes/mhdrunpathfinder/GTFSUtils.kt index 3e593ff..24ea1b5 100644 --- a/app/src/main/java/org/pupes/mhdrunpathfinder/GTFSUtils.kt +++ b/app/src/main/java/org/pupes/mhdrunpathfinder/GTFSUtils.kt @@ -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() + +private data class VehicleUiState(val bearing: Float, val routeShortName: String?) +private val vehicleUiStates = mutableStateMapOf() + +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,36 +137,85 @@ 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() + val updates = mutableListOf() 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) - ) { - 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 + 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(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) { GTFSMainHandler.post { @@ -248,4 +332,7 @@ fun stopGTFSTracking() { } GTFSTrackingThread?.interrupt() GTFSTrackingThread = null + activeVehicleIds.clear() + vehicleUiStates.clear() + gtfsClustererInitialized = false } \ No newline at end of file