Compare commits
No commits in common. "dbd29bf42138765e2077f4b8feab8b2196ab1f56" and "67fea519499972b5d0bd1ff1c2ea363798877342" have entirely different histories.
dbd29bf421
...
67fea51949
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user