add stops

This commit is contained in:
PoliEcho 2026-01-28 19:08:35 +01:00
parent ca2c5fe385
commit ced8e8173d
5 changed files with 128 additions and 37 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
test-data

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-01-19T17:27:16.298528612Z"> <DropdownSelection timestamp="2026-01-28T17:49:25.357991153Z">
<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" />

View File

@ -10,24 +10,28 @@ import androidx.compose.ui.Modifier
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 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.addLazyLoader
import ovh.plrapps.mapcompose.api.addMarker import ovh.plrapps.mapcompose.api.addMarker
import ovh.plrapps.mapcompose.ui.state.markers.model.RenderingStrategy
import java.net.URL import java.net.URL
@Serializable @Serializable
data class GTFSRoute( data class GTFSRoute(
val agency_id: String, val agency_id: String?,
val is_night: Boolean, val is_night: Boolean,
val route_color: String, val route_color: String?,
val route_desc: String? = null, val route_desc: String? = null,
val route_id: String, val route_id: String?,
val route_long_name: String, val route_long_name: String?,
val route_short_name: String, val route_short_name: String?,
val route_text_color: String, val route_text_color: String?,
val route_type: Int, val route_type: Int,
val route_url: String, val route_url: String?,
val is_regional: Boolean, val is_regional: Boolean,
val is_substitute_transport: Boolean val is_substitute_transport: Boolean
) )
@ -40,7 +44,7 @@ var gtfsRoutes: GTFSRoutes? = null
@Serializable @Serializable
data class GTFSStopGeometry( data class GTFSStopGeometry(
val coordinates: List<Double>, val coordinates: List<Double>,
val type: String val type: String?
) )
@Serializable @Serializable
@ -49,7 +53,7 @@ data class GTFSStopProperties(
val parent_station: String? = null, val parent_station: String? = null,
val platform_code: String? = null, val platform_code: String? = null,
val stop_id: String, val stop_id: String,
val stop_name: String, val stop_name: String?,
val wheelchair_boarding: Int, val wheelchair_boarding: Int,
val zone_id: String? = null, val zone_id: String? = null,
val level_id: String? = null val level_id: String? = null
@ -59,13 +63,13 @@ data class GTFSStopProperties(
data class GTFSStop( data class GTFSStop(
val geometry: GTFSStopGeometry, val geometry: GTFSStopGeometry,
val properties: GTFSStopProperties, val properties: GTFSStopProperties,
val type: String val type: String?
) )
@Serializable @Serializable
data class GTFSStops( data class GTFSStops(
val features: List<GTFSStop>, val features: List<GTFSStop>,
val type: String val type: String?
) )
var gtfsStops: GTFSStops? = null var gtfsStops: GTFSStops? = null
@ -113,6 +117,8 @@ fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
var gtfsUrl = URL(url); var gtfsUrl = URL(url);
try { try {
if(gtfsRoutes == null) { if(gtfsRoutes == null) {
GTFSMainHandler.post {
Toast.makeText(callee, "Fetching routes...", Toast.LENGTH_SHORT).show()}
val request = Request.Builder() val request = Request.Builder()
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.routes_path)}") .url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.routes_path)}")
.header( .header(
@ -130,6 +136,8 @@ fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
gtfsRoutes = json.decodeFromString<GTFSRoutes>(message) gtfsRoutes = json.decodeFromString<GTFSRoutes>(message)
}} }}
if(gtfsStops == null) { if(gtfsStops == null) {
GTFSMainHandler.post {
Toast.makeText(callee, "Fetching stops...", Toast.LENGTH_SHORT).show()}
val request = Request.Builder() val request = Request.Builder()
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.stops_path)}") .url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.stops_path)}")
.header( .header(
@ -147,22 +155,30 @@ fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
gtfsStops = json.decodeFromString<GTFSStops>(message) gtfsStops = json.decodeFromString<GTFSStops>(message)
// put all stops on the map // put all stops on the map
GTFSMainHandler.post {
Toast.makeText(callee, "Drawing stops...", Toast.LENGTH_SHORT).show()}
for (stop in gtfsStops!!.features) { for (stop in gtfsStops!!.features) {
GTFSMainHandler.post {
mapState.addMarker(stop.properties.stop_id, x = longitudeToXNormalized(stop.geometry.coordinates[0]), y = latitudeToYNormalized(stop.geometry.coordinates[0])) { if (stop.properties.parent_station == null) {
mapState.addMarker(stop.properties.stop_id, x = longitudeToXNormalized(stop.geometry.coordinates[0]), y = latitudeToYNormalized(stop.geometry.coordinates[1]), renderingStrategy = RenderingStrategy.LazyLoading("default")) {
Icon( Icon(
painter = painterResource(id = R.drawable.outline_directions_bus_24), painter = painterResource(id = R.drawable.outline_directions_bus_24),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(17.dp), modifier = Modifier.size(12.dp),
tint = Color(0xFF0000FF) tint = Color(0xFF0000FF)
) )
} }}
}
} }
GTFSMainHandler.post {
Toast.makeText(callee, "Done drawing stops", Toast.LENGTH_SHORT).show()}
}} }}
} catch(e: Exception) { } catch(e: Exception) {
GTFSMainHandler.post {
Toast.makeText(callee, e.message ?: "Unknown error", Toast.LENGTH_SHORT).show() Toast.makeText(callee, e.message ?: "Unknown error", Toast.LENGTH_SHORT).show()
}
GTFSMainHandler.post { stopGTFSTracking() } GTFSMainHandler.post { stopGTFSTracking() }
return@Thread return@Thread
} }
@ -179,6 +195,7 @@ fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
fun stopGTFSTracking() { fun stopGTFSTracking() {
GTFSIsTracking = false GTFSIsTracking = false
GTFSTrackingThread?.interrupt() GTFSTrackingThread?.interrupt()
GTFSTrackingThread = null GTFSTrackingThread = null
} }

View File

@ -40,11 +40,15 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.edit import androidx.core.content.edit
import ovh.plrapps.mapcompose.api.addLayer import ovh.plrapps.mapcompose.api.addLayer
import ovh.plrapps.mapcompose.api.addLazyLoader
import ovh.plrapps.mapcompose.api.addMarker import ovh.plrapps.mapcompose.api.addMarker
import ovh.plrapps.mapcompose.api.scrollTo import ovh.plrapps.mapcompose.api.scrollTo
import ovh.plrapps.mapcompose.core.TileStreamProvider import ovh.plrapps.mapcompose.core.TileStreamProvider
import ovh.plrapps.mapcompose.ui.MapUI import ovh.plrapps.mapcompose.ui.MapUI
import ovh.plrapps.mapcompose.ui.state.MapState import ovh.plrapps.mapcompose.ui.state.MapState
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.net.URL import java.net.URL
@ -59,7 +63,69 @@ val mapState = MapState(
tileSize = 256 tileSize = 256
) )
/**
* Creates a cached tile stream provider for OpenStreetMap tiles
* Tiles are cached in the app's cache directory for offline access and faster loading
* Cache expires after 30 days to ensure map updates are fetched
*/
fun createCachedTileStreamProvider(context: Context, cacheExpiryDays: Int = 30): TileStreamProvider {
val cacheDir = File(context.cacheDir, "map_tiles")
if (!cacheDir.exists()) {
cacheDir.mkdirs()
}
val cacheExpiryMillis = cacheExpiryDays * 24 * 60 * 60 * 1000L
return TileStreamProvider { row, col, zoomLvl ->
try {
// Create a unique filename for this tile
val tileFile = File(cacheDir, "tile_${zoomLvl}_${col}_${row}.png")
// Check if tile exists in cache and is not expired
val shouldUseCache = tileFile.exists() &&
(System.currentTimeMillis() - tileFile.lastModified()) < cacheExpiryMillis
if (shouldUseCache) {
// Use cached tile
FileInputStream(tileFile)
} else {
// Download tile from OpenStreetMap (either new or expired)
val url = URL("https://tile.openstreetmap.org/$zoomLvl/$col/$row.png")
val connection = url.openConnection()
// Set User-Agent header as required by OSM tile usage policy
val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android) (Contact: poliecho@pupes.org)"
connection.setRequestProperty("User-Agent", userAgent)
val inputStream = connection.getInputStream()
// Save to cache (overwrites if expired)
val outputStream = FileOutputStream(tileFile)
inputStream.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
// Return the cached file's input stream
FileInputStream(tileFile)
}
} catch (e: Exception) {
// If download fails but we have a cached tile (even if expired), use it
val tileFile = File(cacheDir, "tile_${zoomLvl}_${col}_${row}.png")
if (tileFile.exists()) {
try {
FileInputStream(tileFile)
} catch (fallbackError: Exception) {
e.printStackTrace()
null
}
} else {
e.printStackTrace()
null
}
}
}
}
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -95,15 +161,16 @@ fun MainScreen(callee: Activity) {
var haukUrl by remember { mutableStateOf("") } var haukUrl by remember { mutableStateOf("") }
var golemioAPIkey by remember { mutableStateOf(sharedPreferences.getString("golemioAPIkey", "")?: "" ) } var golemioAPIkey by remember { mutableStateOf(sharedPreferences.getString("golemioAPIkey", "")?: "" ) }
var haukState by remember { mutableStateOf(false) } var haukState by remember { mutableStateOf(false) }
var golemioState by remember { mutableStateOf(false)} var golemioState by remember { mutableStateOf(golemioAPIkey.isNotEmpty())}
if (golemioAPIkey.isNotEmpty()) { LaunchedEffect(Unit) {
golemioState = true if (golemioState) {
startGTFSTracking(callee.getString(R.string.golemio_base_url), golemioAPIkey, callee); startGTFSTracking(callee.getString(R.string.golemio_base_url), golemioAPIkey, callee)
}
} }
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
OpenStreetMapScreen() OpenStreetMapScreen(callee)
SettingsButton( SettingsButton(
onClick = { showSettings = true }, onClick = { showSettings = true },
modifier = Modifier modifier = Modifier
@ -211,6 +278,7 @@ fun ShowSettingsMenu(
value = haukUrl, value = haukUrl,
onValueChange = onHaukUrlChange, onValueChange = onHaukUrlChange,
label = { Text("Target Hauk URL") }, label = { Text("Target Hauk URL") },
singleLine = true,
modifier = Modifier modifier = Modifier
.width(250.dp) .width(250.dp)
) )
@ -224,6 +292,7 @@ fun ShowSettingsMenu(
onCheckedChange = { onGolemioStateChange(it) } onCheckedChange = { onGolemioStateChange(it) }
) )
TextField(value = golemioAPIkey, onValueChange = onGolemioAPIChange, label = { Text("Golemio API Key") }, TextField(value = golemioAPIkey, onValueChange = onGolemioAPIChange, label = { Text("Golemio API Key") },
singleLine = true,
modifier = Modifier modifier = Modifier
.width(250.dp)); .width(250.dp));
ElevatedButton(onClick = onGolemioApply, modifier = Modifier.padding(start = 5.dp), shape = RectangleShape) { ElevatedButton(onClick = onGolemioApply, modifier = Modifier.padding(start = 5.dp), shape = RectangleShape) {
@ -235,24 +304,27 @@ fun ShowSettingsMenu(
@Preview @Preview
@Composable @Composable
fun OpenStreetMapScreen() { fun OpenStreetMapScreen(context: Context? = null) {
// Create tile stream provider for OpenStreetMap with User-Agent // Create tile stream provider for OpenStreetMap with caching
val tileStreamProvider = TileStreamProvider { row, col, zoomLvl -> val tileStreamProvider = remember(context) {
try { context?.let {
val url = URL("https://tile.openstreetmap.org/$zoomLvl/$col/$row.png") createCachedTileStreamProvider(it)
val connection = url.openConnection() } ?: TileStreamProvider { row, col, zoomLvl ->
// Set User-Agent header as required by OSM tile usage policy // Fallback for preview mode without context
// Format: AppId/Version (Platform) (Contact: email) try {
val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android) (Contact: poliecho@pupes.org)" val url = URL("https://tile.openstreetmap.org/$zoomLvl/$col/$row.png")
connection.setRequestProperty("User-Agent", userAgent) val connection = url.openConnection()
connection.getInputStream() val userAgent = "MHDrunPathfinder/Preview (Android)"
} catch (e: Exception) { connection.setRequestProperty("User-Agent", userAgent)
e.printStackTrace() connection.getInputStream()
null } catch (e: Exception) {
e.printStackTrace()
null
}
} }
} }
mapState.addLazyLoader("default");
/* Add a marker at the center of the map */ /* Add a marker at the center of the map */
mapState.addMarker("target_position", x = longitudeToXNormalized(14.4058031), y = latitudeToYNormalized(50.0756083)) { mapState.addMarker("target_position", x = longitudeToXNormalized(14.4058031), y = latitudeToYNormalized(50.0756083)) {

View File

@ -3,4 +3,5 @@
<string name="routes_path">/v2/gtfs/routes</string> <string name="routes_path">/v2/gtfs/routes</string>
<string name="stops_path">/v2/gtfs/stops</string> <string name="stops_path">/v2/gtfs/stops</string>
<string name="golemio_base_url">https://api.golemio.cz</string> <string name="golemio_base_url">https://api.golemio.cz</string>
<string name="golemio_realtime_path"></string>
</resources> </resources>