Compare commits

...
2 Commits
Author SHA1 Message Date
PoliEcho 7ad24463b7 better Public transport vehicle map 2026-09-06 13:15:28 +02:00
PoliEcho a7f2be8a3e add vehicles 2026-09-03 17:19:17 +02:00
3 changed files with 140 additions and 5 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-09-02T15:28:00.985067432Z"> <DropdownSelection timestamp="2026-09-03T13:26:15.706271055Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/lukas/.config/.android/avd/Medium_Phone_OSS.avd" /> <DeviceId pluginId="LocalEmulator" identifier="path=/home/lukas/.config/.android/avd/Medium_Phone_OSS.avd" />
@@ -4,12 +4,34 @@ import android.app.Activity
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.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 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.hasMarker
import ovh.plrapps.mapcompose.api.moveMarker
import ovh.plrapps.mapcompose.api.removeMarker import ovh.plrapps.mapcompose.api.removeMarker
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(
@@ -74,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 {
@@ -88,15 +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()) {
val id = entity.vehicle.vehicle.id
// use vehicle 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()),
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) { } catch (e: Exception) {
GTFSMainHandler.post { GTFSMainHandler.post {
@@ -213,4 +332,7 @@ fun stopGTFSTracking() {
} }
GTFSTrackingThread?.interrupt() GTFSTrackingThread?.interrupt()
GTFSTrackingThread = null GTFSTrackingThread = null
activeVehicleIds.clear()
vehicleUiStates.clear()
gtfsClustererInitialized = false
} }
+13
View File
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M2.98,253.02H253.02V128L128,2.98 2.98,128v125.02"
android:strokeLineJoin="round"
android:strokeWidth="5.95207"
android:fillColor="#000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>