Compare commits

..

No commits in common. "dbd29bf42138765e2077f4b8feab8b2196ab1f56" and "67fea519499972b5d0bd1ff1c2ea363798877342" have entirely different histories.

5 changed files with 131 additions and 204 deletions

View File

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

View File

@ -49,38 +49,15 @@ private fun fetchHaukPosition(haukUrl: String, callee: Activity) {
message = "{" + message.substringAfter("{");
val hauk_response = json.decodeFromString<HaukResponse>(message)
val lat = hauk_response.points.last().getOrNull(0)
val lon = hauk_response.points.last().getOrNull(1)
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
}
val lat = hauk_response.points.last()?.getOrNull(0)
val lon = hauk_response.points.last()?.getOrNull(1)
if (lat != null && lon != null) {
HaukMainHandler.post {
try {
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 {
HaukMainHandler.post {
Toast.makeText(callee, "TARGET LOCATION NOT AVAILABLE", Toast.LENGTH_SHORT).show()
}
}}
} catch (e: Exception) {
// Post UI update to main thread

View File

@ -4,27 +4,6 @@ import kotlinx.serialization.json.Json
import kotlin.math.PI
import kotlin.math.ln
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 {
coerceInputValues = true // Coerce nulls to default values if applicable

View File

@ -2,6 +2,7 @@ package org.pupes.mhdrunpathfinder
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@ -29,10 +30,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import kotlinx.coroutines.launch
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
@ -51,12 +50,15 @@ import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.net.URL
import kotlinx.coroutines.*
import java.util.concurrent.Executors
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
// Max zoom level is 18, tile size is 256x256
// At zoom level 18, there are 2^18 = 262144 tiles in each dimension
@ -67,12 +69,12 @@ val mapState = MapState(
fullHeight = 67108864,
tileSize = 256
)
var target_speed by mutableStateOf("0.0km/h");
/**
* 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
* Uses background thread pool to prevent ANR (Application Not Responding)
*/
fun createCachedTileStreamProvider(context: Context, cacheExpiryDays: Int = 30): TileStreamProvider {
val cacheDir = File(context.cacheDir, "map_tiles")
@ -84,40 +86,65 @@ fun createCachedTileStreamProvider(context: Context, cacheExpiryDays: Int = 30):
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) {
return@TileStreamProvider FileInputStream(tileFile)
}
// Since mapcompose runs this on a background thread, we can do networking here.
// Use cached tile
FileInputStream(tileFile)
} else {
// Download tile from OpenStreetMap in background thread
val future = tileDownloadExecutor.submit<FileInputStream?> {
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
val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android) (Contact: poliecho@pupes.org)"
connection.setRequestProperty("User-Agent", userAgent)
connection.connectTimeout = 5000
connection.readTimeout = 10000
connection.connectTimeout = 5000 // 5 second timeout
connection.readTimeout = 10000 // 10 second timeout
connection.getInputStream().use { inputStream ->
FileOutputStream(tileFile).use { outputStream ->
inputStream.copyTo(outputStream)
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) {
e.printStackTrace()
// If download fails, try to use an expired cached tile if it exists
null
}
}
// 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()) {
FileInputStream(tileFile)
} else {
null
}
}
} catch (e: Exception) {
// If download timed out or failed, try using expired cache
if (tileFile.exists()) {
FileInputStream(tileFile)
} else {
null
}
}
}
} catch (e: Exception) {
e.printStackTrace()
null
@ -170,7 +197,6 @@ class MainActivity : ComponentActivity() {
@Composable
fun MainScreen(callee: Activity) {
val sharedPreferences = remember { callee.getSharedPreferences("PREFERENCES", Context.MODE_PRIVATE) }
val coroutineScope = rememberCoroutineScope()
var showSettings by remember { mutableStateOf(false) }
var haukUrl by remember { mutableStateOf("") }
var golemioAPIkey by remember { mutableStateOf(sharedPreferences.getString("golemioAPIkey", "")?: "" ) }
@ -185,40 +211,12 @@ fun MainScreen(callee: Activity) {
Box(modifier = Modifier.fillMaxSize()) {
OpenStreetMapScreen(callee)
IconButton( // settings button
SettingsButton(
onClick = { showSettings = true },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(end = 5.dp, bottom = 5.dp),
IconID = R.drawable.baseline_settings_24
.padding(end = 5.dp, bottom = 5.dp)
)
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) {
ShowSettingsMenu(
onDismiss = { showSettings = false },
@ -277,7 +275,7 @@ fun MainScreen(callee: Activity) {
@Composable
// When a composable with parameters is used with @Preview,
// a default value must be provided for the preview to render.
fun IconButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}, IconID: Int = R.drawable.baseline_settings_24) {
fun SettingsButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
Button(
onClick = { onClick() },
colors = ButtonDefaults.buttonColors(containerColor = Color(0xCFFFFFFF)),
@ -285,7 +283,7 @@ fun IconButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}, IconID:
) {
Icon(
painter = painterResource(id = IconID),
painter = painterResource(id = R.drawable.baseline_settings_24),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color(0xFF000000)
@ -366,56 +364,32 @@ fun OpenStreetMapScreen(context: Context? = null) {
}
}
// Track if map has been initialized to prevent duplicate operations
var mapInitialized by remember { mutableStateOf(false) }
mapState.addLazyLoader("default");
// Add the tile layer, markers, and set initial position - all in LaunchedEffect to avoid blocking main thread
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) {
/* Add a marker at the center of the map */
mapState.addMarker("target_position", x = longitudeToXNormalized(14.4058031), y = latitudeToYNormalized(50.0756083)) {
Icon(
painter = painterResource(id = R.drawable.target),
contentDescription = null,
modifier = Modifier.size(24.dp),
modifier = Modifier.size(20.dp),
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 */
mapState.addMarker("current_position", x = longitudeToXNormalized(14.0), y = latitudeToYNormalized(50.0),
Offset(-0.5f,-0.5f)
) {
mapState.addMarker("current_position", x = longitudeToXNormalized(14.4378), y = latitudeToYNormalized(50.0755)) {
Icon(
painter = painterResource(id = R.drawable.user_location),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color(0xFF004802)
modifier = Modifier.size(20.dp),
tint = Color(0xFF0000FF) // Blue color for current position
)
}
// Add the tile layer and set initial position
LaunchedEffect(Unit) {
mapState.addLayer(tileStreamProvider)
// Scroll to Prague, Czech Republic
// Prague coordinates: latitude 50.0755, longitude 14.4378
val normalizedX = longitudeToXNormalized(14.4378)
@ -423,9 +397,6 @@ fun OpenStreetMapScreen(context: Context? = null) {
// Use a higher scale to zoom in more (closer to 1.0 = more zoomed in)
mapState.scrollTo(normalizedX, normalizedY, destScale = 0.1)
mapInitialized = true
}
}
// Display the map