Compare commits
2
Commits
a2c5f1aac3
...
7ad24463b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad24463b7 | ||
|
|
a7f2be8a3e |
Generated
+1
-1
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user