Compare commits

..

4 Commits

Author SHA1 Message Date
dbd29bf421 add working speed-o-meter 2026-02-13 16:41:05 +01:00
c3f8dd4066 revert GTFSUtils.kt from 67fea519 2026-02-13 14:37:17 +01:00
5970dce0d2 works? 2026-02-13 14:20:30 +01:00
0e2208d104 add more optimalization and speed mareker 2026-02-13 12:28:19 +01:00
5 changed files with 204 additions and 131 deletions

View File

@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-28T21:00:38.307279245Z"> <DropdownSelection timestamp="2026-02-13T13:44:07.200825088Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=BS98317AA1341400032" /> <DeviceId pluginId="LocalEmulator" identifier="path=/home/lukas/.config/.android/avd/Medium_Phone_OSS.avd" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@ -49,15 +49,38 @@ private fun fetchHaukPosition(haukUrl: String, callee: Activity) {
message = "{" + message.substringAfter("{"); message = "{" + message.substringAfter("{");
val hauk_response = json.decodeFromString<HaukResponse>(message) val hauk_response = json.decodeFromString<HaukResponse>(message)
val lat = hauk_response.points.last().getOrNull(0)
val lat = hauk_response.points.last()?.getOrNull(0) val lon = hauk_response.points.last().getOrNull(1)
val lon = hauk_response.points.last()?.getOrNull(1)
if (lat != null && lon != null) { if (lat != null && lon != null) {
val lastlat = hauk_response.points.getOrNull(hauk_response.points.size - 2)?.getOrNull(0)
val lastlon = hauk_response.points.getOrNull(hauk_response.points.size - 2)?.getOrNull(1)
val newspeed =
if (lastlat != null && lastlon != null) {
var fulldistance: Double = 0.0
for (i in 1 until hauk_response.points.size -1) {
try{
fulldistance += haversineDistance(hauk_response.points[i-1][0]!!, hauk_response.points[i-1][1]!!, hauk_response.points[i][0]!!, hauk_response.points[i][1]!!)
} catch (e: Exception) {
// Ignore
}
}
(fulldistance / (hauk_response.interval * hauk_response.points.size)*3600)
} else {
0.0
}
HaukMainHandler.post { HaukMainHandler.post {
try {
mapState.moveMarker("target_position", x = longitudeToXNormalized(lon), y = latitudeToYNormalized(lat)) mapState.moveMarker("target_position", x = longitudeToXNormalized(lon), y = latitudeToYNormalized(lat))
target_speed = "${"%.1f".format(newspeed)}km/h"
} catch (e: Exception) {
// Marker might not exist yet, ignore
}
} }
} else { } else {
HaukMainHandler.post {
Toast.makeText(callee, "TARGET LOCATION NOT AVAILABLE", Toast.LENGTH_SHORT).show() Toast.makeText(callee, "TARGET LOCATION NOT AVAILABLE", Toast.LENGTH_SHORT).show()
}
}} }}
} catch (e: Exception) { } catch (e: Exception) {
// Post UI update to main thread // Post UI update to main thread

View File

@ -4,6 +4,27 @@ import kotlinx.serialization.json.Json
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.ln import kotlin.math.ln
import kotlin.math.tan import kotlin.math.tan
import kotlin.math.*
fun haversineDistance(
lat1: Double,
lon1: Double,
lat2: Double,
lon2: Double
): Double {
val R = 6371.0 // Earth radius in kilometers
val phi1 = lat1 * PI / 180
val phi2 = lat2 * PI / 180
val deltaPhi = (lat2 - lat1) * PI / 180
val deltaLambda = (lon2 - lon1) * PI / 180
val a = sin(deltaPhi / 2).pow(2) +
cos(phi1) * cos(phi2) * sin(deltaLambda / 2).pow(2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return R * c
}
val json = Json { val json = Json {
coerceInputValues = true // Coerce nulls to default values if applicable coerceInputValues = true // Coerce nulls to default values if applicable

View File

@ -2,7 +2,6 @@ package org.pupes.mhdrunpathfinder
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -30,8 +29,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import kotlinx.coroutines.launch
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
@ -50,15 +51,12 @@ import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.net.URL import java.net.URL
import kotlinx.coroutines.*
import java.util.concurrent.Executors
import android.os.StrictMode import android.os.StrictMode
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.sp
import ovh.plrapps.mapcompose.api.getMarkerInfo
// Thread pool for tile downloading to prevent blocking UI thread
private val tileDownloadExecutor = Executors.newFixedThreadPool(
(Runtime.getRuntime().availableProcessors() - 3).coerceAtLeast(1)
)
// MapState configuration for OpenStreetMap // MapState configuration for OpenStreetMap
// Max zoom level is 18, tile size is 256x256 // Max zoom level is 18, tile size is 256x256
// At zoom level 18, there are 2^18 = 262144 tiles in each dimension // At zoom level 18, there are 2^18 = 262144 tiles in each dimension
@ -69,12 +67,12 @@ val mapState = MapState(
fullHeight = 67108864, fullHeight = 67108864,
tileSize = 256 tileSize = 256
) )
var target_speed by mutableStateOf("0.0km/h");
/** /**
* Creates a cached tile stream provider for OpenStreetMap tiles * Creates a cached tile stream provider for OpenStreetMap tiles
* Tiles are cached in the app's cache directory for offline access and faster loading * 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 * Cache expires after 30 days to ensure map updates are fetched
* Uses background thread pool to prevent ANR (Application Not Responding)
*/ */
fun createCachedTileStreamProvider(context: Context, cacheExpiryDays: Int = 30): TileStreamProvider { fun createCachedTileStreamProvider(context: Context, cacheExpiryDays: Int = 30): TileStreamProvider {
val cacheDir = File(context.cacheDir, "map_tiles") val cacheDir = File(context.cacheDir, "map_tiles")
@ -86,65 +84,40 @@ fun createCachedTileStreamProvider(context: Context, cacheExpiryDays: Int = 30):
return TileStreamProvider { row, col, zoomLvl -> return TileStreamProvider { row, col, zoomLvl ->
try { try {
// Create a unique filename for this tile
val tileFile = File(cacheDir, "tile_${zoomLvl}_${col}_${row}.png") val tileFile = File(cacheDir, "tile_${zoomLvl}_${col}_${row}.png")
// Check if tile exists in cache and is not expired
val shouldUseCache = tileFile.exists() && val shouldUseCache = tileFile.exists() &&
(System.currentTimeMillis() - tileFile.lastModified()) < cacheExpiryMillis (System.currentTimeMillis() - tileFile.lastModified()) < cacheExpiryMillis
if (shouldUseCache) { if (shouldUseCache) {
// Use cached tile return@TileStreamProvider FileInputStream(tileFile)
FileInputStream(tileFile) }
} else {
// Download tile from OpenStreetMap in background thread // Since mapcompose runs this on a background thread, we can do networking here.
val future = tileDownloadExecutor.submit<FileInputStream?> {
try { try {
val url = URL("https://tile.openstreetmap.org/$zoomLvl/$col/$row.png") val url = URL("https://tile.openstreetmap.org/$zoomLvl/$col/$row.png")
val connection = url.openConnection() val connection = url.openConnection()
// Set User-Agent header as required by OSM tile usage policy // Set User-Agent header as required by OSM tile usage policy
val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android) (Contact: poliecho@pupes.org)" val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android) (Contact: poliecho@pupes.org)"
connection.setRequestProperty("User-Agent", userAgent) connection.setRequestProperty("User-Agent", userAgent)
connection.connectTimeout = 5000 // 5 second timeout connection.connectTimeout = 5000
connection.readTimeout = 10000 // 10 second timeout connection.readTimeout = 10000
val inputStream = connection.getInputStream() connection.getInputStream().use { inputStream ->
FileOutputStream(tileFile).use { outputStream ->
// Save to cache (overwrites if expired) inputStream.copyTo(outputStream)
val outputStream = FileOutputStream(tileFile)
inputStream.use { input ->
outputStream.use { output ->
input.copyTo(output)
} }
} }
// Return the cached file's input stream
FileInputStream(tileFile) FileInputStream(tileFile)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null // If download fails, try to use an expired cached tile if it exists
}
}
// Wait for download with timeout
try {
future.get() ?: run {
// If download failed but we have a cached tile (even if expired), use it
if (tileFile.exists()) { if (tileFile.exists()) {
FileInputStream(tileFile) FileInputStream(tileFile)
} else { } else {
null null
} }
} }
} catch (e: Exception) {
// If download timed out or failed, try using expired cache
if (tileFile.exists()) {
FileInputStream(tileFile)
} else {
null
}
}
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
null null
@ -197,6 +170,7 @@ class MainActivity : ComponentActivity() {
@Composable @Composable
fun MainScreen(callee: Activity) { fun MainScreen(callee: Activity) {
val sharedPreferences = remember { callee.getSharedPreferences("PREFERENCES", Context.MODE_PRIVATE) } val sharedPreferences = remember { callee.getSharedPreferences("PREFERENCES", Context.MODE_PRIVATE) }
val coroutineScope = rememberCoroutineScope()
var showSettings by remember { mutableStateOf(false) } var showSettings by remember { mutableStateOf(false) }
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", "")?: "" ) }
@ -211,12 +185,40 @@ fun MainScreen(callee: Activity) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
OpenStreetMapScreen(callee) OpenStreetMapScreen(callee)
SettingsButton( IconButton( // settings button
onClick = { showSettings = true }, onClick = { showSettings = true },
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(end = 5.dp, bottom = 5.dp) .padding(end = 5.dp, bottom = 5.dp),
IconID = R.drawable.baseline_settings_24
) )
Column(modifier = Modifier.align(Alignment.TopStart)) {
IconButton( // look at target button
onClick = {
coroutineScope.launch {
mapState.getMarkerInfo("target_position")?.let { marker ->
mapState.scrollTo(marker.x, marker.y, destScale = 0.1)
}
}
},
modifier = Modifier
.padding(start = 5.dp, bottom = 5.dp),
IconID = R.drawable.target
)
IconButton( // look at my location button
onClick = {
coroutineScope.launch {
mapState.getMarkerInfo("current_position")?.let { marker ->
mapState.scrollTo(marker.x, marker.y, destScale = 0.1)
}
}
},
modifier = Modifier
.padding(start = 5.dp, bottom = 5.dp),
IconID = R.drawable.user_location
)
}
if (showSettings) { if (showSettings) {
ShowSettingsMenu( ShowSettingsMenu(
onDismiss = { showSettings = false }, onDismiss = { showSettings = false },
@ -275,7 +277,7 @@ fun MainScreen(callee: Activity) {
@Composable @Composable
// When a composable with parameters is used with @Preview, // When a composable with parameters is used with @Preview,
// a default value must be provided for the preview to render. // a default value must be provided for the preview to render.
fun SettingsButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) { fun IconButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}, IconID: Int = R.drawable.baseline_settings_24) {
Button( Button(
onClick = { onClick() }, onClick = { onClick() },
colors = ButtonDefaults.buttonColors(containerColor = Color(0xCFFFFFFF)), colors = ButtonDefaults.buttonColors(containerColor = Color(0xCFFFFFFF)),
@ -283,7 +285,7 @@ fun SettingsButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.baseline_settings_24), painter = painterResource(id = IconID),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
tint = Color(0xFF000000) tint = Color(0xFF000000)
@ -364,32 +366,56 @@ fun OpenStreetMapScreen(context: Context? = null) {
} }
} }
mapState.addLazyLoader("default"); // Track if map has been initialized to prevent duplicate operations
var mapInitialized by remember { mutableStateOf(false) }
/* Add a marker at the center of the map */ // Add the tile layer, markers, and set initial position - all in LaunchedEffect to avoid blocking main thread
mapState.addMarker("target_position", x = longitudeToXNormalized(14.4058031), y = latitudeToYNormalized(50.0756083)) { LaunchedEffect(Unit) {
if (!mapInitialized) {
// Add lazy loader first
mapState.addLazyLoader("default")
// Add tile layer
mapState.addLayer(tileStreamProvider)
// Add a marker for target position
mapState.addMarker("target_position", x = longitudeToXNormalized(14.0), y = latitudeToYNormalized(50.0), Offset(-0.5f,-0.25f)) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon( Icon(
painter = painterResource(id = R.drawable.target), painter = painterResource(id = R.drawable.target),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(20.dp), modifier = Modifier.size(24.dp),
tint = Color(0xFFFF0000) tint = Color(0xFFFF0000)
) )
Surface(
color = Color.Red,
shadowElevation = 4.dp,
shape = MaterialTheme.shapes.small,
modifier = Modifier.padding(1.dp)
) {
Text(
text = target_speed,
modifier = Modifier.padding(1.dp),
style = MaterialTheme.typography.bodyMedium,
fontSize = 8.sp,
color = Color.Blue
)
}
}
} }
/* Add a marker for current position */ /* Add a marker for current position */
mapState.addMarker("current_position", x = longitudeToXNormalized(14.4378), y = latitudeToYNormalized(50.0755)) { mapState.addMarker("current_position", x = longitudeToXNormalized(14.0), y = latitudeToYNormalized(50.0),
Offset(-0.5f,-0.5f)
) {
Icon( Icon(
painter = painterResource(id = R.drawable.user_location), painter = painterResource(id = R.drawable.user_location),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(20.dp), modifier = Modifier.size(24.dp),
tint = Color(0xFF0000FF) // Blue color for current position tint = Color(0xFF004802)
) )
} }
// Add the tile layer and set initial position
LaunchedEffect(Unit) {
mapState.addLayer(tileStreamProvider)
// Scroll to Prague, Czech Republic // Scroll to Prague, Czech Republic
// Prague coordinates: latitude 50.0755, longitude 14.4378 // Prague coordinates: latitude 50.0755, longitude 14.4378
val normalizedX = longitudeToXNormalized(14.4378) val normalizedX = longitudeToXNormalized(14.4378)
@ -397,6 +423,9 @@ fun OpenStreetMapScreen(context: Context? = null) {
// Use a higher scale to zoom in more (closer to 1.0 = more zoomed in) // Use a higher scale to zoom in more (closer to 1.0 = more zoomed in)
mapState.scrollTo(normalizedX, normalizedY, destScale = 0.1) mapState.scrollTo(normalizedX, normalizedY, destScale = 0.1)
mapInitialized = true
}
} }
// Display the map // Display the map