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
.cxx
local.properties
test-data

View File

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

View File

@ -40,11 +40,15 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import ovh.plrapps.mapcompose.api.addLayer
import ovh.plrapps.mapcompose.api.addLazyLoader
import ovh.plrapps.mapcompose.api.addMarker
import ovh.plrapps.mapcompose.api.scrollTo
import ovh.plrapps.mapcompose.core.TileStreamProvider
import ovh.plrapps.mapcompose.ui.MapUI
import ovh.plrapps.mapcompose.ui.state.MapState
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.net.URL
@ -59,7 +63,69 @@ val mapState = MapState(
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() {
@ -95,15 +161,16 @@ fun MainScreen(callee: Activity) {
var haukUrl by remember { mutableStateOf("") }
var golemioAPIkey by remember { mutableStateOf(sharedPreferences.getString("golemioAPIkey", "")?: "" ) }
var haukState by remember { mutableStateOf(false) }
var golemioState by remember { mutableStateOf(false)}
var golemioState by remember { mutableStateOf(golemioAPIkey.isNotEmpty())}
if (golemioAPIkey.isNotEmpty()) {
golemioState = true
startGTFSTracking(callee.getString(R.string.golemio_base_url), golemioAPIkey, callee);
LaunchedEffect(Unit) {
if (golemioState) {
startGTFSTracking(callee.getString(R.string.golemio_base_url), golemioAPIkey, callee)
}
}
Box(modifier = Modifier.fillMaxSize()) {
OpenStreetMapScreen()
OpenStreetMapScreen(callee)
SettingsButton(
onClick = { showSettings = true },
modifier = Modifier
@ -211,6 +278,7 @@ fun ShowSettingsMenu(
value = haukUrl,
onValueChange = onHaukUrlChange,
label = { Text("Target Hauk URL") },
singleLine = true,
modifier = Modifier
.width(250.dp)
)
@ -224,6 +292,7 @@ fun ShowSettingsMenu(
onCheckedChange = { onGolemioStateChange(it) }
)
TextField(value = golemioAPIkey, onValueChange = onGolemioAPIChange, label = { Text("Golemio API Key") },
singleLine = true,
modifier = Modifier
.width(250.dp));
ElevatedButton(onClick = onGolemioApply, modifier = Modifier.padding(start = 5.dp), shape = RectangleShape) {
@ -235,24 +304,27 @@ fun ShowSettingsMenu(
@Preview
@Composable
fun OpenStreetMapScreen() {
// Create tile stream provider for OpenStreetMap with User-Agent
val tileStreamProvider = TileStreamProvider { row, col, zoomLvl ->
try {
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
// Format: AppId/Version (Platform) (Contact: email)
val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android) (Contact: poliecho@pupes.org)"
connection.setRequestProperty("User-Agent", userAgent)
connection.getInputStream()
} catch (e: Exception) {
e.printStackTrace()
null
fun OpenStreetMapScreen(context: Context? = null) {
// Create tile stream provider for OpenStreetMap with caching
val tileStreamProvider = remember(context) {
context?.let {
createCachedTileStreamProvider(it)
} ?: TileStreamProvider { row, col, zoomLvl ->
// Fallback for preview mode without context
try {
val url = URL("https://tile.openstreetmap.org/$zoomLvl/$col/$row.png")
val connection = url.openConnection()
val userAgent = "MHDrunPathfinder/Preview (Android)"
connection.setRequestProperty("User-Agent", userAgent)
connection.getInputStream()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
mapState.addLazyLoader("default");
/* Add a marker at the center of the map */
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="stops_path">/v2/gtfs/stops</string>
<string name="golemio_base_url">https://api.golemio.cz</string>
<string name="golemio_realtime_path"></string>
</resources>