Compare commits
9 Commits
ca2c5fe385
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| dbd29bf421 | |||
| c3f8dd4066 | |||
| 5970dce0d2 | |||
| 0e2208d104 | |||
| 67fea51949 | |||
| 4263a7eff0 | |||
| d1314a1b32 | |||
| 93148fcf36 | |||
| ced8e8173d |
@@ -13,3 +13,4 @@
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
test-data
|
||||
|
||||
Generated
+1
-1
@@ -4,7 +4,7 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-01-19T17:27:16.298528612Z">
|
||||
<DropdownSelection timestamp="2026-02-13T13:44:07.200825088Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/home/lukas/.config/.android/avd/Medium_Phone_OSS.avd" />
|
||||
|
||||
@@ -14,33 +14,36 @@ import kotlinx.serialization.Serializable
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import ovh.plrapps.mapcompose.api.addMarker
|
||||
import ovh.plrapps.mapcompose.api.removeMarker
|
||||
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
|
||||
)
|
||||
|
||||
// Type alias for array of GTFS routes
|
||||
typealias GTFSRoutes = List<GTFSRoute>
|
||||
typealias GTFSRoutes = MutableList<GTFSRoute>
|
||||
|
||||
var gtfsRoutes: GTFSRoutes = mutableListOf()
|
||||
|
||||
var gtfsRoutes: GTFSRoutes? = null
|
||||
|
||||
@Serializable
|
||||
data class GTFSStopGeometry(
|
||||
val coordinates: List<Double>,
|
||||
val type: String
|
||||
val type: String?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -49,7 +52,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,16 +62,16 @@ 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 features: MutableList<GTFSStop>,
|
||||
val type: String?
|
||||
)
|
||||
|
||||
var gtfsStops: GTFSStops? = null
|
||||
var gtfsStops: GTFSStops = GTFSStops(mutableListOf<GTFSStop>(), "FeatureCollection")
|
||||
|
||||
|
||||
// OkHttpClient for making HTTP requests
|
||||
@@ -104,34 +107,24 @@ private fun fetchGTFSPosition(gtfsUrl: URL,api_key: String, callee: Activity) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Start the timer with a URL:
|
||||
fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
|
||||
if (GTFSIsTracking) return
|
||||
|
||||
GTFSIsTracking = true
|
||||
GTFSTrackingThread = Thread {
|
||||
var gtfsUrl = URL(url);
|
||||
val gtfsUrl = URL(url);
|
||||
try {
|
||||
if(gtfsRoutes == null) {
|
||||
val request = Request.Builder()
|
||||
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.routes_path)}")
|
||||
.header(
|
||||
"User-Agent",
|
||||
"${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android)"
|
||||
)
|
||||
.header("X-Access-Token", api_key).build();
|
||||
if(gtfsRoutes.isEmpty()) {
|
||||
GTFSMainHandler.post {
|
||||
Toast.makeText(callee, "Fetching routes...", Toast.LENGTH_SHORT).show()}
|
||||
|
||||
gtfshttpClient.newCall(request).execute().use { response ->
|
||||
var message = if (response.isSuccessful) {
|
||||
response.body?.string() ?: "Empty response"
|
||||
} else {
|
||||
"Error: ${response.code} ${response.message}"
|
||||
}
|
||||
gtfsRoutes = json.decodeFromString<GTFSRoutes>(message)
|
||||
}}
|
||||
if(gtfsStops == null) {
|
||||
var messageParsed: GTFSRoutes;
|
||||
var offset = 0;
|
||||
do {
|
||||
val request = Request.Builder()
|
||||
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.stops_path)}")
|
||||
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.routes_path)}?offset=${offset}")
|
||||
.header(
|
||||
"User-Agent",
|
||||
"${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android)"
|
||||
@@ -144,27 +137,63 @@ fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
|
||||
} else {
|
||||
"Error: ${response.code} ${response.message}"
|
||||
}
|
||||
gtfsStops = json.decodeFromString<GTFSStops>(message)
|
||||
messageParsed = json.decodeFromString<GTFSRoutes>(message)
|
||||
gtfsRoutes.addAll(messageParsed)
|
||||
offset += messageParsed.size
|
||||
}} while (messageParsed.isNotEmpty())
|
||||
}
|
||||
if(gtfsStops.features.isEmpty()) {
|
||||
GTFSMainHandler.post {
|
||||
Toast.makeText(callee, "Fetching stops...", Toast.LENGTH_SHORT).show()}
|
||||
|
||||
// put all stops on the map
|
||||
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])) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.outline_directions_bus_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(17.dp),
|
||||
tint = Color(0xFF0000FF)
|
||||
)
|
||||
}
|
||||
var messageParsed: GTFSStops;
|
||||
var offset = 0;
|
||||
do {
|
||||
val request = Request.Builder()
|
||||
.url("${gtfsUrl.protocol}://${gtfsUrl.host}${callee.getString(R.string.stops_path)}?offset=${offset}")
|
||||
.header(
|
||||
"User-Agent",
|
||||
"${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (Android)"
|
||||
)
|
||||
.header("X-Access-Token", api_key).build();
|
||||
|
||||
}
|
||||
gtfshttpClient.newCall(request).execute().use { response ->
|
||||
var message = if (response.isSuccessful) {
|
||||
response.body?.string() ?: "Empty response"
|
||||
} else {
|
||||
"Error: ${response.code} ${response.message}"
|
||||
}
|
||||
messageParsed = json.decodeFromString<GTFSStops>(message)
|
||||
gtfsStops.features.addAll(messageParsed.features)
|
||||
}
|
||||
|
||||
offset += messageParsed.features.size;
|
||||
}while(messageParsed.features.isNotEmpty())
|
||||
}
|
||||
// put all stops on the map
|
||||
GTFSMainHandler.post {
|
||||
Toast.makeText(callee, "Drawing stops...", Toast.LENGTH_SHORT).show()}
|
||||
for (stop in gtfsStops!!.features) {
|
||||
|
||||
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(12.dp),
|
||||
tint = Color(0xFF0000FF)
|
||||
)
|
||||
}}
|
||||
} catch(e: Exception) {
|
||||
}
|
||||
GTFSMainHandler.post {
|
||||
Toast.makeText(callee, "Done drawing ${gtfsStops.features.size} 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
|
||||
}
|
||||
GTFSMainHandler.post { stopGTFSTracking() }
|
||||
return@Thread
|
||||
}
|
||||
while (GTFSIsTracking) {
|
||||
fetchGTFSPosition(gtfsUrl,api_key, callee)
|
||||
@@ -179,6 +208,11 @@ fun startGTFSTracking(url: String, api_key: String, callee: Activity) {
|
||||
|
||||
fun stopGTFSTracking() {
|
||||
GTFSIsTracking = false
|
||||
for (stop in gtfsStops!!.features) {
|
||||
if (stop.properties.parent_station == null) {
|
||||
mapState.removeMarker(stop.properties.stop_id);
|
||||
}
|
||||
}
|
||||
GTFSTrackingThread?.interrupt()
|
||||
GTFSTrackingThread = null
|
||||
}
|
||||
@@ -49,15 +49,38 @@ 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)
|
||||
val lat = hauk_response.points.last().getOrNull(0)
|
||||
val lon = hauk_response.points.last().getOrNull(1)
|
||||
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 {
|
||||
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) {
|
||||
// Post UI update to main thread
|
||||
|
||||
@@ -4,6 +4,27 @@ 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,7 +2,6 @@ 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
|
||||
@@ -30,8 +29,10 @@ 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
|
||||
@@ -40,12 +41,20 @@ 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
|
||||
import android.os.StrictMode
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ovh.plrapps.mapcompose.api.getMarkerInfo
|
||||
|
||||
|
||||
// MapState configuration for OpenStreetMap
|
||||
@@ -58,14 +67,84 @@ 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
|
||||
*/
|
||||
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 {
|
||||
val tileFile = File(cacheDir, "tile_${zoomLvl}_${col}_${row}.png")
|
||||
|
||||
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.
|
||||
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.getInputStream().use { inputStream ->
|
||||
FileOutputStream(tileFile).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
FileInputStream(tileFile)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// If download fails, try to use an expired cached tile if it exists
|
||||
if (tileFile.exists()) {
|
||||
FileInputStream(tileFile)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Configure StrictMode to detect threading violations (debug mode only)
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setThreadPolicy(
|
||||
StrictMode.ThreadPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog() // Log violations instead of crashing
|
||||
.build()
|
||||
)
|
||||
StrictMode.setVmPolicy(
|
||||
StrictMode.VmPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
@@ -91,25 +170,55 @@ 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", "")?: "" ) }
|
||||
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()
|
||||
SettingsButton(
|
||||
OpenStreetMapScreen(callee)
|
||||
IconButton( // settings button
|
||||
onClick = { showSettings = true },
|
||||
modifier = Modifier
|
||||
.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) {
|
||||
ShowSettingsMenu(
|
||||
onDismiss = { showSettings = false },
|
||||
@@ -168,7 +277,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 SettingsButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
fun IconButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}, IconID: Int = R.drawable.baseline_settings_24) {
|
||||
Button(
|
||||
onClick = { onClick() },
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xCFFFFFFF)),
|
||||
@@ -176,7 +285,7 @@ fun SettingsButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.baseline_settings_24),
|
||||
painter = painterResource(id = IconID),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color(0xFF000000)
|
||||
@@ -211,6 +320,7 @@ fun ShowSettingsMenu(
|
||||
value = haukUrl,
|
||||
onValueChange = onHaukUrlChange,
|
||||
label = { Text("Target Hauk URL") },
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.width(250.dp)
|
||||
)
|
||||
@@ -224,6 +334,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,56 +346,86 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 */
|
||||
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
|
||||
// Add the tile layer, markers, and set initial position - all in LaunchedEffect to avoid blocking main thread
|
||||
LaunchedEffect(Unit) {
|
||||
mapState.addLayer(tileStreamProvider)
|
||||
if (!mapInitialized) {
|
||||
// Add lazy loader first
|
||||
mapState.addLazyLoader("default")
|
||||
|
||||
// Scroll to Prague, Czech Republic
|
||||
// Prague coordinates: latitude 50.0755, longitude 14.4378
|
||||
val normalizedX = longitudeToXNormalized(14.4378)
|
||||
val normalizedY = latitudeToYNormalized(50.0755)
|
||||
// Add tile layer
|
||||
mapState.addLayer(tileStreamProvider)
|
||||
|
||||
// Use a higher scale to zoom in more (closer to 1.0 = more zoomed in)
|
||||
mapState.scrollTo(normalizedX, normalizedY, destScale = 0.8)
|
||||
// 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(
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user