Compare commits
4 Commits
67fea51949
...
dbd29bf421
| Author | SHA1 | Date | |
|---|---|---|---|
| dbd29bf421 | |||
| c3f8dd4066 | |||
| 5970dce0d2 | |||
| 0e2208d104 |
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@ -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>
|
||||||
|
|||||||
@ -116,39 +116,39 @@ fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
|
|||||||
GTFSTrackingThread = Thread {
|
GTFSTrackingThread = Thread {
|
||||||
val gtfsUrl = URL(url);
|
val gtfsUrl = URL(url);
|
||||||
try {
|
try {
|
||||||
if(gtfsRoutes.isEmpty()) {
|
if(gtfsRoutes.isEmpty()) {
|
||||||
GTFSMainHandler.post {
|
GTFSMainHandler.post {
|
||||||
Toast.makeText(callee, "Fetching routes...", Toast.LENGTH_SHORT).show()}
|
Toast.makeText(callee, "Fetching routes...", Toast.LENGTH_SHORT).show()}
|
||||||
|
|
||||||
var messageParsed: GTFSRoutes;
|
var messageParsed: GTFSRoutes;
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
do {
|
do {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.routes_path)}?offset=${offset}")
|
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.routes_path)}?offset=${offset}")
|
||||||
.header(
|
.header(
|
||||||
"User-Agent",
|
"User-Agent",
|
||||||
"${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android)"
|
"${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android)"
|
||||||
)
|
)
|
||||||
.header("X-Access-Token", api_key).build();
|
.header("X-Access-Token", api_key).build();
|
||||||
|
|
||||||
gtfshttpClient.newCall(request).execute().use { response ->
|
gtfshttpClient.newCall(request).execute().use { response ->
|
||||||
var message = if (response.isSuccessful) {
|
var message = if (response.isSuccessful) {
|
||||||
response.body?.string() ?: "Empty response"
|
response.body?.string() ?: "Empty response"
|
||||||
} else {
|
} else {
|
||||||
"Error: ${response.code} ${response.message}"
|
"Error: ${response.code} ${response.message}"
|
||||||
}
|
}
|
||||||
messageParsed = json.decodeFromString<GTFSRoutes>(message)
|
messageParsed = json.decodeFromString<GTFSRoutes>(message)
|
||||||
gtfsRoutes.addAll(messageParsed)
|
gtfsRoutes.addAll(messageParsed)
|
||||||
offset += messageParsed.size
|
offset += messageParsed.size
|
||||||
}} while (messageParsed.isNotEmpty())
|
}} while (messageParsed.isNotEmpty())
|
||||||
}
|
}
|
||||||
if(gtfsStops.features.isEmpty()) {
|
if(gtfsStops.features.isEmpty()) {
|
||||||
GTFSMainHandler.post {
|
GTFSMainHandler.post {
|
||||||
Toast.makeText(callee, "Fetching stops...", Toast.LENGTH_SHORT).show()}
|
Toast.makeText(callee, "Fetching stops...", Toast.LENGTH_SHORT).show()}
|
||||||
|
|
||||||
var messageParsed: GTFSStops;
|
var messageParsed: GTFSStops;
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
do {
|
do {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.stops_path)}?offset=${offset}")
|
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.stops_path)}?offset=${offset}")
|
||||||
.header(
|
.header(
|
||||||
@ -168,8 +168,8 @@ fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
offset += messageParsed.features.size;
|
offset += messageParsed.features.size;
|
||||||
}while(messageParsed.features.isNotEmpty())
|
}while(messageParsed.features.isNotEmpty())
|
||||||
}
|
}
|
||||||
// put all stops on the map
|
// put all stops on the map
|
||||||
GTFSMainHandler.post {
|
GTFSMainHandler.post {
|
||||||
Toast.makeText(callee, "Drawing stops...", Toast.LENGTH_SHORT).show()}
|
Toast.makeText(callee, "Drawing stops...", Toast.LENGTH_SHORT).show()}
|
||||||
@ -188,12 +188,12 @@ fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
|
|||||||
GTFSMainHandler.post {
|
GTFSMainHandler.post {
|
||||||
Toast.makeText(callee, "Done drawing ${gtfsStops.features.size} stops", Toast.LENGTH_SHORT).show()}
|
Toast.makeText(callee, "Done drawing ${gtfsStops.features.size} stops", Toast.LENGTH_SHORT).show()}
|
||||||
|
|
||||||
} catch(e: Exception) {
|
} catch(e: Exception) {
|
||||||
GTFSMainHandler.post {
|
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
|
||||||
}
|
}
|
||||||
while (GTFSIsTracking) {
|
while (GTFSIsTracking) {
|
||||||
fetchGTFSPosition(gtfsUrl,api_key, callee)
|
fetchGTFSPosition(gtfsUrl,api_key, callee)
|
||||||
|
|||||||
@ -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) {
|
||||||
HaukMainHandler.post {
|
|
||||||
mapState.moveMarker("target_position", x = longitudeToXNormalized(lon), y = latitudeToYNormalized(lat))
|
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 {
|
||||||
|
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 {
|
} else {
|
||||||
Toast.makeText(callee, "TARGET LOCATION NOT AVAILABLE", Toast.LENGTH_SHORT).show()
|
HaukMainHandler.post {
|
||||||
|
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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,63 +84,38 @@ 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
|
|
||||||
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 // 5 second timeout
|
|
||||||
connection.readTimeout = 10000 // 10 second timeout
|
|
||||||
|
|
||||||
val inputStream = connection.getInputStream()
|
// Since mapcompose runs this on a background thread, we can do networking here.
|
||||||
|
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
|
||||||
|
|
||||||
// Save to cache (overwrites if expired)
|
connection.getInputStream().use { inputStream ->
|
||||||
val outputStream = FileOutputStream(tileFile)
|
FileOutputStream(tileFile).use { outputStream ->
|
||||||
inputStream.use { input ->
|
inputStream.copyTo(outputStream)
|
||||||
outputStream.use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the cached file's input stream
|
|
||||||
FileInputStream(tileFile)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
FileInputStream(tileFile)
|
||||||
// Wait for download with timeout
|
} catch (e: Exception) {
|
||||||
try {
|
e.printStackTrace()
|
||||||
future.get() ?: run {
|
// If download fails, try to use an expired cached tile if it exists
|
||||||
// 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) {
|
||||||
@ -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,39 +366,66 @@ 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)) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.target),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = Color(0xFFFF0000)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add a marker for current position */
|
|
||||||
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(20.dp),
|
|
||||||
tint = Color(0xFF0000FF) // Blue color for current position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the tile layer and set initial position
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
mapState.addLayer(tileStreamProvider)
|
if (!mapInitialized) {
|
||||||
|
// Add lazy loader first
|
||||||
|
mapState.addLazyLoader("default")
|
||||||
|
|
||||||
// Scroll to Prague, Czech Republic
|
// Add tile layer
|
||||||
// Prague coordinates: latitude 50.0755, longitude 14.4378
|
mapState.addLayer(tileStreamProvider)
|
||||||
val normalizedX = longitudeToXNormalized(14.4378)
|
|
||||||
val normalizedY = latitudeToYNormalized(50.0755)
|
|
||||||
|
|
||||||
// Use a higher scale to zoom in more (closer to 1.0 = more zoomed in)
|
// Add a marker for target position
|
||||||
mapState.scrollTo(normalizedX, normalizedY, destScale = 0.1)
|
mapState.addMarker("target_position", x = longitudeToXNormalized(14.0), y = latitudeToYNormalized(50.0), Offset(-0.5f,-0.25f)) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.target),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.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)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.user_location),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = Color(0xFF004802)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to Prague, Czech Republic
|
||||||
|
// Prague coordinates: latitude 50.0755, longitude 14.4378
|
||||||
|
val normalizedX = longitudeToXNormalized(14.4378)
|
||||||
|
val normalizedY = latitudeToYNormalized(50.0755)
|
||||||
|
|
||||||
|
// 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
|
// Display the map
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user