Bimba.git

commit 512a04241931d21c9f626dc5fa1cfd1dacdb6128

Author: Adam Evyčędo <git@apiote.xyz>

add transitous departures

%!v(PANIC=String method: strings: negative Repeat count)


diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 91096abf593d7294f7fb9a4c254b0b069b7e539e..7f86f61f618002f8ad0f054cf998b1ef07de87e2 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -39,8 +39,8 @@ 		resValue("string", "applicationId", applicationId)
 	}
 
 	compileOptions {
-		sourceCompatibility = JavaVersion.VERSION_17
-		targetCompatibility = JavaVersion.VERSION_17
+		sourceCompatibility = JavaVersion.VERSION_21
+		targetCompatibility = JavaVersion.VERSION_21
 		isCoreLibraryDesugaringEnabled = true
 	}
 




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/queryables.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/queryables.kt
deleted file mode 100644
index fe0b7bde7d55f820581244d91dd74a775ab0b19f..0000000000000000000000000000000000000000
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/queryables.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-package xyz.apiote.bimba.czwek.api
-
-import android.content.Context
-import android.os.Build
-import android.util.JsonReader
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import xyz.apiote.bimba.czwek.R
-import xyz.apiote.bimba.czwek.api.responses.ErrorResponse
-import xyz.apiote.bimba.czwek.repo.Position
-import xyz.apiote.bimba.czwek.repo.Queryable
-import xyz.apiote.bimba.czwek.repo.Stop
-import xyz.apiote.bimba.czwek.repo.TrafficResponseException
-import java.io.IOException
-import java.net.HttpURLConnection
-import java.net.URL
-
-suspend fun getTransitousQueryables(query: String, context: Context): List<Queryable> {
-	if (!isNetworkAvailable(context)) {
-		throw TrafficResponseException(0, "", Error(0, R.string.error_offline, R.drawable.error_net))
-	}
-
-	val json = """
-		{
-			"destination": {
-			"type": "Module",
-			"target": "/guesser"
-		},
-		"content_type": "StationGuesserRequest",
-		"content": {
-			"input": "$query",
-			"guess_count": 6
-		}
-	}"""
-	val result = withContext(Dispatchers.IO) {
-		val url = URL("https://routing.spline.de/api/")
-		val c = (url.openConnection() as HttpURLConnection).apply {
-			setRequestProperty(
-				"User-Agent",
-				"${context.getString(R.string.applicationId)}/${context.getString(R.string.versionName)} (${Build.VERSION.SDK_INT})"
-			)
-			addRequestProperty("Content-Type", "application/json")
-			requestMethod = "POST"
-			doOutput = true
-			outputStream.write(json.toByteArray())
-		}
-		try {
-			if (c.responseCode == 200) {
-				Result(c.inputStream, null)
-			} else {
-				val (string, image) = mapHttpError(c.responseCode)
-				Result(c.errorStream, Error(c.responseCode, string, image))
-			}
-		} catch (e: IOException) {
-			Result(null, Error(0, R.string.error_connecting, R.drawable.error_server))
-		}
-	}
-
-	if (result.error != null) {
-		if (result.stream != null) {
-			val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) }
-			throw TrafficResponseException(result.error.statusCode, response.message, result.error)
-		} else {
-			throw TrafficResponseException(result.error.statusCode, "", result.error)
-		}
-	} else {
-		return withContext(Dispatchers.IO) {
-			val queryables = mutableListOf<Queryable>()
-			val r = JsonReader(result.stream!!.bufferedReader())
-			r.beginObject()
-			while (r.hasNext()) {
-				val rootFieldName = r.nextName() // content
-				if (rootFieldName == "content") {
-					r.beginObject()
-					r.nextName() // guesses
-					r.beginArray()
-					while (r.hasNext()) {
-						var id = ""
-						var name = ""
-						var latitude = 0.0
-						var longitude = 0.0
-						r.beginObject()
-						while (r.hasNext()) {
-							val fieldName = r.nextName()
-							when (fieldName) {
-								"id" -> id = r.nextString()
-								"name" -> name = r.nextString()
-								"pos" -> {
-									r.beginObject()
-									while (r.hasNext()) {
-										val positionFieldName = r.nextName()
-										when (positionFieldName) {
-											"lat" -> latitude = r.nextDouble()
-											"lng" -> longitude = r.nextDouble()
-										}
-									}
-									r.endObject()
-								}
-							}
-						}
-						r.endObject()
-						queryables.add(
-							Stop(
-								code = id,
-								name = name,
-								nodeName = name,
-								zone = "",
-								feedID = "transitous",
-								position = Position(latitude = latitude, longitude = longitude),
-								changeOptions = emptyList()
-							)
-						)
-					}
-					r.endArray()
-					r.endObject()
-				} else {
-					r.skipValue()
-				}
-			}
-			r.endObject()
-
-			return@withContext queryables
-		}
-	}
-}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/settings.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/settings.kt
deleted file mode 100644
index 2e53c8f207147ae333a35de77d9854e0e3c2c4e6..0000000000000000000000000000000000000000
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/settings.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package xyz.apiote.bimba.czwek.api
-
-import android.content.Context
-import androidx.preference.PreferenceManager
-
-fun isTransitousEnabled(context: Context): Boolean {
-	return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("transitous_enabled", false)
-}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitous.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitous.kt
index 4c6e3eeeab7d96968d1041e248f5aa80c61feb91..e566210b9774cd999d009f32c9b501433dfdf403 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitous.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitous.kt
@@ -37,4 +37,4 @@ 		} catch (e: IOException) {
 			Result(null, Error(0, R.string.error_connecting, R.drawable.error_server))
 		}
 	}
-}
\ No newline at end of file
+}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt
index ae907d44738c5b7f79e98c31e48bfe6baef69ca5..66fa6ae249f756a186b7405f5794b29726efd1db 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt
@@ -278,4 +278,4 @@ 		f(i)
 		i++
 	}
 	endArray()
-}
\ No newline at end of file
+}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousQueryables.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousQueryables.kt
index b76c39cdaf563c228ecbc91128e104282fe603c9..1397e0c211c2b4109a72c05b554404717cdb9874 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousQueryables.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousQueryables.kt
@@ -191,4 +191,4 @@ 			r.endObject()
 			return@withContext queryables
 		}
 	}
-}
\ No newline at end of file
+}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousSettings.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousSettings.kt
index b976816c7ce659a9814b7b8a44dbca884ef30ffd..b135a0c210d068dcde94ba25b825e54ded1ea350 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousSettings.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousSettings.kt
@@ -9,4 +9,4 @@ import androidx.preference.PreferenceManager
 
 fun isTransitousEnabled(context: Context): Boolean {
 	return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("transitous_enabled", false)
-}
\ No newline at end of file
+}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt
index 49d071f82a3db9cb92cc640eb441c6263a570a5a..4b740bd0b13df60606a1ab0eec3e0f1e884a2a39 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt
@@ -16,4 +16,19 @@ 		rgb = (rgb shl 8) + G.toInt()
 		rgb = (rgb shl 8) + B.toInt()
 		return rgb
 	}
+
+	companion object {
+		fun fromHex(hex: String): Colour {
+			return hex.removePrefix("#").let {
+				if (it.isEmpty()) {
+					Colour(255u, 255u, 255u)
+				} else {
+					val r = it.substring(0 until 2).toUByte(16)
+					val g = it.substring(2 until 4).toUByte(16)
+					val b = it.substring(4 until 6).toUByte(16)
+					Colour(r, g, b)
+				}
+			}
+		}
+	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt
index 95fa5fa0c75f0c3e765eefd64c22091021260eb8..a337b0178c144f066ffea44ebd3898c475d850c0 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt
@@ -9,7 +9,9 @@ import android.graphics.drawable.Drawable
 import xyz.apiote.bimba.czwek.api.Server
 import java.time.LocalDate
 
-interface Queryable
+interface Queryable {
+	fun location(): Position?
+}
 interface Locatable {
 	fun icon(context: Context, scale: Float = 1f): Drawable
 	fun location(): Position




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Line.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Line.kt
index 7a28e2b3e2128782b096ffc2744719151997fe65..6e7822ae6190905832576ddb5d78fd01a8bb1576 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Line.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Line.kt
@@ -53,4 +53,8 @@
 	fun icon(context: Context, scale: Float = 1f): Drawable {
 		return BitmapDrawable(context.resources, super.icon(context, type, colour, scale))
 	}
+
+	override fun location(): Position? {
+		return null
+	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineAbstract.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineAbstract.kt
index db2525fa84c91a88fb53b7d6c1b40de93540f8d7..8cfbfc83fdca77fbde3ae8077f613596bc0317cf 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineAbstract.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineAbstract.kt
@@ -72,6 +72,7 @@ 			LineType.CABLE_CAR -> R.drawable.cabletram_black
 			LineType.FUNICULAR -> R.drawable.funicular_black
 			LineType.MONORAIL -> R.drawable.monorail_black
 			LineType.UNKNOWN -> R.drawable.vehicle_black
+			LineType.PLANE -> R.drawable.plane_black
 		}
 		val icon = AppCompatResources.getDrawable(context, iconID)?.mutate()?.apply {
 			setTint(textColour(colour))




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineType.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineType.kt
index 8dca46f48cb8703d00c19d9a86e0c2dd246dece4..46be84493816f7bcf0e5e8d25416344dc81b6172 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineType.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineType.kt
@@ -9,7 +9,7 @@ import xyz.apiote.bimba.czwek.api.LineTypeV2
 import xyz.apiote.bimba.czwek.api.LineTypeV3
 
 enum class LineType {
-	UNKNOWN, TRAM, BUS, TROLLEYBUS, METRO, RAIL, FERRY, CABLE_TRAM, CABLE_CAR, FUNICULAR, MONORAIL;
+	UNKNOWN, TRAM, BUS, TROLLEYBUS, METRO, RAIL, FERRY, CABLE_TRAM, CABLE_CAR, FUNICULAR, MONORAIL, PLANE;
 
 	companion object {
 		fun of(t: LineTypeV1): LineType {
@@ -40,6 +40,25 @@ 				LineTypeV3.CABLE_TRAM -> valueOf("CABLE_TRAM")
 				LineTypeV3.CABLE_CAR -> valueOf("CABLE_CAR")
 				LineTypeV3.FUNICULAR -> valueOf("FUNICULAR")
 				LineTypeV3.MONORAIL -> valueOf("MONORAIL")
+			}
+		}
+
+		fun fromTransitous(clasz: Int): LineType {
+			return when (clasz) {
+				0 -> PLANE
+				1 -> RAIL
+				2 -> RAIL
+				3 -> BUS
+				4 -> RAIL
+				5 -> RAIL
+				6 -> RAIL
+				7 -> METRO
+				8 -> METRO
+				9 -> TRAM
+				10 -> BUS
+				11 -> FERRY
+				12 -> UNKNOWN // other
+				else -> UNKNOWN
 			}
 		}
 	}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt
index f8126a10ef5aafae21a5afbbc30bbcca87a1f49e..b11a5dfcccc9048e0390e0dfe9ad530e875a7668 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt
@@ -16,6 +16,7 @@ import xyz.apiote.fruchtfleisch.Writer
 import java.io.File
 import java.net.URLEncoder
 import java.time.LocalDate
+import xyz.apiote.bimba.czwek.R
 
 class OfflineRepository(context: Context) : Repository {
 	private val db =
@@ -103,9 +104,21 @@ 			s
 		}
 
 		// XXX `on conflict` is not supported on older versions of Android
-		val cursor = db.rawQuery("select * from favourites where feed_id = ? and stop_code = ?", arrayOf(favourite.feedID, favourite.stopCode))
+		val cursor = db.rawQuery(
+			"select * from favourites where feed_id = ? and stop_code = ?",
+			arrayOf(favourite.feedID, favourite.stopCode)
+		)
 		if (cursor.count > 0) {
-			db.execSQL("update favourites set stop_name = ?, lines = ?, sequence = ? where feed_id = ? and stop_code = ?", arrayOf(favourite.stopName, favourite.lines.joinToString(separator = "||"), favourite.sequence, favourite.feedID, favourite.stopCode))
+			db.execSQL(
+				"update favourites set stop_name = ?, lines = ?, sequence = ? where feed_id = ? and stop_code = ?",
+				arrayOf(
+					favourite.stopName,
+					favourite.lines.joinToString(separator = "||"),
+					favourite.sequence,
+					favourite.feedID,
+					favourite.stopCode
+				)
+			)
 		} else {
 			db.execSQL(
 				"insert into favourites(sequence, feed_id, feed_name, stop_code, stop_name, lines) values (?, ?,?,?,?,?)",
@@ -162,7 +175,23 @@ 			if (version.toUInt() != FeedInfo.VERSION) {
 				saveFeedCache(context, feeds)
 			}
 		}
-		return feeds
+		val feedsWithTransitous = feeds.toMutableMap()
+		feedsWithTransitous.put(
+			"transitous", FeedInfo(
+				"transitous",
+				"Transitous",
+				context.getString(R.string.transitous_description),
+				context.getString(R.string.transitous_attribution),
+				LocalDate.now(),
+				"",
+				QrLocation.NONE,
+				"",
+				null,
+				null,
+				false
+			)
+		)
+		return feedsWithTransitous
 	}
 
 	override suspend fun getDepartures(




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
index 6255bb1ab1f911681dd3577064d557f01667a668..64f5498c6cdb53cc8340ee59a386083a6233db8b 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
@@ -7,6 +7,7 @@
 import android.content.Context
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
+import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.api.LineV1
 import xyz.apiote.bimba.czwek.api.LineV2
 import xyz.apiote.bimba.czwek.api.LineV3
@@ -19,6 +20,9 @@ import xyz.apiote.bimba.czwek.api.UnknownResourceException
 import xyz.apiote.bimba.czwek.api.VehicleV1
 import xyz.apiote.bimba.czwek.api.VehicleV2
 import xyz.apiote.bimba.czwek.api.VehicleV3
+import xyz.apiote.bimba.czwek.api.getTransitousDepartures
+import xyz.apiote.bimba.czwek.api.getTransitousQueryables
+import xyz.apiote.bimba.czwek.api.locateTransitousQueryables
 import xyz.apiote.bimba.czwek.api.responses.DeparturesResponse
 import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseDev
 import xyz.apiote.bimba.czwek.api.responses.DeparturesResponseV1
@@ -46,7 +50,6 @@ import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV1
 import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV2
 import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV3
 import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV4
-import xyz.apiote.bimba.czwek.api.getTransitousQueryables
 import java.time.LocalDate
 
 // todo [3.2] in Repository check if responses are BARE or HTML
@@ -83,7 +86,7 @@ 				throw TrafficResponseException(result.error.statusCode, "", result.error)
 			}
 		} else {
 			val rawResponse = result.stream!!.readBytes()
-			return when (val response =
+			val feeds = when (val response =
 				withContext(Dispatchers.IO) { FeedsResponse.unmarshal(rawResponse.inputStream()) }) {
 				is FeedsResponseDev -> response.feeds.associate { Pair(it.id, FeedInfo(it)) }
 				is FeedsResponseV2 -> response.feeds.associate { Pair(it.id, FeedInfo(it)) }
@@ -91,6 +94,16 @@ 				is FeedsResponseV1 -> response.feeds.associate { Pair(it.id, FeedInfo(it)) }
 
 				else -> null
 			}
+			val feedsWithTransitous = feeds?.toMutableMap()
+			feedsWithTransitous?.put(
+				"transitous", FeedInfo(
+					"transitous", "Transitous",
+					context.getString(R.string.transitous_description),
+					context.getString(R.string.transitous_attribution),
+					LocalDate.now(), "", QrLocation.NONE, "", null, null, false
+				)
+			)
+			return feedsWithTransitous
 		}
 	}
 
@@ -101,8 +114,10 @@ 		date: LocalDate?,
 		context: Context,
 		limit: Int?
 	): StopDepartures? {
-		val result =
-			xyz.apiote.bimba.czwek.api.getDepartures(
+		return if (feedID == "transitous") {
+			getTransitousDepartures(context, stop, date, limit)
+		} else {
+			val result = xyz.apiote.bimba.czwek.api.getDepartures(
 				context,
 				Server.get(context),
 				feedID,
@@ -110,42 +125,44 @@ 				stop,
 				date,
 				limit
 			)
-		if (result.error != null) {
-			if (result.stream != null) {
-				val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) }
-				throw TrafficResponseException(result.error.statusCode, response.message, result.error)
+
+			if (result.error != null) {
+				if (result.stream != null) {
+					val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) }
+					throw TrafficResponseException(result.error.statusCode, response.message, result.error)
+				} else {
+					throw TrafficResponseException(result.error.statusCode, "", result.error)
+				}
 			} else {
-				throw TrafficResponseException(result.error.statusCode, "", result.error)
-			}
-		} else {
-			return when (val response =
-				withContext(Dispatchers.IO) { DeparturesResponse.unmarshal(result.stream!!) }) {
-				is DeparturesResponseDev -> StopDepartures(
-					response.departures.map { Departure(it) },
-					Stop(response.stop),
-					response.alerts.map { Alert(it) })
+				when (val response =
+					withContext(Dispatchers.IO) { DeparturesResponse.unmarshal(result.stream!!) }) {
+					is DeparturesResponseDev -> StopDepartures(
+						response.departures.map { Departure(it) },
+						Stop(response.stop),
+						response.alerts.map { Alert(it) })
 
-				is DeparturesResponseV4 -> StopDepartures(
-					response.departures.map { Departure(it) },
-					Stop(response.stop),
-					response.alerts.map { Alert(it) })
+					is DeparturesResponseV4 -> StopDepartures(
+						response.departures.map { Departure(it) },
+						Stop(response.stop),
+						response.alerts.map { Alert(it) })
 
-				is DeparturesResponseV3 -> StopDepartures(
-					response.departures.map { Departure(it) },
-					Stop(response.stop),
-					response.alerts.map { Alert(it) })
+					is DeparturesResponseV3 -> StopDepartures(
+						response.departures.map { Departure(it) },
+						Stop(response.stop),
+						response.alerts.map { Alert(it) })
 
-				is DeparturesResponseV2 -> StopDepartures(
-					response.departures.map { Departure(it) },
-					Stop(response.stop),
-					response.alerts.map { Alert(it) })
+					is DeparturesResponseV2 -> StopDepartures(
+						response.departures.map { Departure(it) },
+						Stop(response.stop),
+						response.alerts.map { Alert(it) })
 
-				is DeparturesResponseV1 -> StopDepartures(
-					response.departures.map { Departure(it) },
-					Stop(response.stop),
-					response.alerts.map { Alert(it) })
+					is DeparturesResponseV1 -> StopDepartures(
+						response.departures.map { Departure(it) },
+						Stop(response.stop),
+						response.alerts.map { Alert(it) })
 
-				else -> null
+					else -> null
+				}
 			}
 		}
 	}
@@ -235,12 +252,16 @@
 	override suspend fun queryQueryables(
 		query: String, context: Context
 	): List<Queryable>? {
-		val transitousQueryables = //if (isTransitousEnabled(context)) {
+		val transitousQueryables = if (Server.get(context).feeds.transitousEnabled()) {
 			getTransitousQueryables(query, context)
-		/*} else {
+		} else {
+			null
+		}
+		val bimbaQueryables = if (Server.get(context).feeds.bimbaEnabled()) {
+			getQueryables(query, null, context, "query")
+		} else {
 			null
-		}*/
-		val bimbaQueryables = getQueryables(query, null, context, "query")
+		}
 		return if (transitousQueryables == null && bimbaQueryables == null) {
 			null
 		} else {
@@ -251,7 +272,23 @@
 	override suspend fun locateQueryables(
 		position: Position, context: Context
 	): List<Queryable>? {
-		return getQueryables(null, position, context, "locate")
+		val transitousQueryables = if (Server.get(context).feeds.transitousEnabled()) {
+			locateTransitousQueryables(position, context)
+		} else {
+			null
+		}
+		val bimbaQueryables = if (Server.get(context).feeds.bimbaEnabled()) {
+			getQueryables(null, position, context, "locate")
+		} else {
+			null
+		}
+		return if (transitousQueryables == null && bimbaQueryables == null) {
+			null
+		} else {
+			(transitousQueryables ?: listOf()) + (bimbaQueryables ?: listOf()).sortedBy {
+				it.location()?.distanceTo(position) ?: 0f
+			}
+		}
 	}
 
 	private suspend fun getQueryables(




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Position.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Position.kt
index 9b912930f830eb23cee2c74075c318b68186d713..d03cf34ae8cb789df7e078f0f24c509e63343a12 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Position.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Position.kt
@@ -4,11 +4,22 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 package xyz.apiote.bimba.czwek.repo
 
+import android.location.Location
 import xyz.apiote.bimba.czwek.api.PositionV1
 
 data class Position(val latitude: Double, val longitude: Double) {
 	constructor(p: PositionV1) : this(p.latitude, p.longitude)
 	fun isZero(): Boolean {
 		return latitude == 0.0 && longitude == 0.0
+	}
+
+	fun distanceTo(other: Position): Float {
+		return Location(null).apply {
+			latitude = this@Position.latitude
+			longitude = this@Position.longitude
+		}.distanceTo(Location(null).apply {
+			latitude = other.latitude
+			longitude = other.longitude
+		})
 	}
 }
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
index a13611c7ee852e3421c01a0bdc651128d0ef2d6f..57d79cf7d143b6290b74b68ef7848fc69bb782b4 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt
@@ -139,7 +139,7 @@ 				setImageDrawable(stop.icon(context!!))
 				contentDescription = context.getString(R.string.stop_content_description)
 			}
 			holder?.title?.text = stop.name
-			if ((feedsSettings?.activeFeedsCount() ?: 0) > 1) {
+			if ((feedsSettings?.activeFeedsCount() ?: 0) > 1 || (stop.feedID ?: "") == "transitous") {
 				holder?.feedName?.visibility = View.VISIBLE
 				holder?.feedName?.text = feeds?.get(stop.feedID)?.name ?: ""
 			}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt
index 31c0bcd141c540e1b70d418b402b6859ee6a9199..0407d15f16954e42e900ebad7f43b6e268603eec 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedSettings.kt
@@ -17,7 +17,9 @@ @OptIn(ExperimentalStdlibApi::class)
 data class FeedsSettings(val settings: MutableMap<String, FeedSettings>) {
 	fun activeFeedsCount() = settings.count { it.value.enabled && it.value.useOnline }
 	fun activeFeeds() = settings.filter { it.value.enabled && it.value.useOnline }.keys
-	fun getIDs() = activeFeeds().joinToString(",")
+	fun getIDs() = activeFeeds().filter { it != "transitous" }.joinToString(",")
+	fun transitousEnabled() = activeFeeds().contains("transitous")
+	fun bimbaEnabled() = activeFeeds().filter { it != "transitous" }.isNotEmpty()
 
 	fun save(context: Context, server: Server) {
 		val doc = KBson().dump(serializer(), this).toHexString()




diff --git a/app/src/main/res/layout/feed_bottom_sheet.xml b/app/src/main/res/layout/feed_bottom_sheet.xml
index 97c3bd552d9fd51347bba2fa92c98a0068714e9a..f6acca2b1fd45853063220c1f049100a84beb0bb 100644
--- a/app/src/main/res/layout/feed_bottom_sheet.xml
+++ b/app/src/main/res/layout/feed_bottom_sheet.xml
@@ -60,9 +60,9 @@ 	 		android:id="@+id/onlineOfflineDivider"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
+		android:layout_marginTop="16dp"
 		app:dividerInsetEnd="16dp"
 		app:dividerInsetStart="16dp"
-		android:layout_marginTop="16dp"
 		app:layout_constraintTop_toBottomOf="@+id/onlineSwitch" />
 
 	<com.google.android.material.textview.MaterialTextView
@@ -71,8 +71,8 @@ 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_marginTop="8dp"
 		android:text="@string/information_may_be_outdated"
-		android:textStyle="italic"
 		android:textAppearance="@style/TextAppearance.Material3.BodySmall"
+		android:textStyle="italic"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toBottomOf="@+id/onlineOfflineDivider" />
@@ -82,8 +82,9 @@ 		android:id="@+id/description"
 		android:layout_width="0dp"
 		android:layout_height="wrap_content"
 		android:layout_marginStart="8dp"
+		android:layout_marginTop="16dp"
 		android:layout_marginEnd="8dp"
-		android:layout_marginTop="16dp"
+		android:autoLink="web"
 		android:textAlignment="center"
 		android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
 		app:layout_constraintEnd_toEndOf="parent"
@@ -96,23 +97,24 @@ 		android:id="@+id/timetable_validity"
 		android:layout_width="0dp"
 		android:layout_height="wrap_content"
 		android:layout_marginStart="8dp"
-		android:textAlignment="center"
+		android:layout_marginTop="16dp"
 		android:layout_marginEnd="8dp"
+		android:textAlignment="center"
+		android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
 		android:visibility="gone"
-		android:layout_marginTop="16dp"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
 		app:layout_constraintTop_toBottomOf="@+id/description"
 		tool:text="Current timetable valid: 2024-01-01 to 2024-02-01" />
 
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/attribution"
-		android:layout_marginTop="16dp"
 		android:layout_width="0dp"
 		android:layout_height="wrap_content"
 		android:layout_marginStart="8dp"
+		android:layout_marginTop="16dp"
 		android:layout_marginEnd="8dp"
+		android:autoLink="web"
 		android:textAlignment="center"
 		android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
 		app:layout_constraintEnd_toEndOf="parent"




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0f28969a1ce9661561811d143e015aedfe141d66..e7276841464dd7fc779cddd62a31bc9b3402309d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -285,4 +285,6 @@ 	show
 	<string name="terminus_arrival_showing">Terminus arrivals</string>
 	<string name="matrix_button_description">link to Matrix channel</string>
 	<string name="email_button_description">link to email</string>
+	<string name="transitous_description">A community-run provider-neutral international public transport routing service. Coverage is available at https://transitous.org/sources/</string>
+	<string name="transitous_attribution">Transitous (https://transitous.org) API provided by Spline (https://routing.spline.de). Localities (https://github.com/public-transport/transitous/tree/main/feeds) maintained by the community.</string>
 </resources>




diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml
index 229ed1c026cdbd5babade118fd129c6f22f72f1a..1f2aa2c99cc598244676fb0917da77ac392dfb6f 100644
--- a/app/src/main/res/values-en-rUS/strings.xml
+++ b/app/src/main/res/values-en-rUS/strings.xml
@@ -144,17 +144,17 @@ 	%1$s towards %2$s
 	<string name="departure_headsign">» %1$s</string>
 	<string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) based on Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n Mastodon icon (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Bimba logo created by https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Map data © OpenStreetMap contributors (https://www.openstreetmap.org/copyright), ODbL-1.0\n\n Cities list used for geocoding short plus codes © Geonames (https://geonames.org), CC BY\n\n Matrix logo ™/® Matrix.org</string>
 	<string name="title_about">About</string>
-	<string name="translation_button_description">" Mastodon icon (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later"</string>
+	<string name="translation_button_description">link to translations service</string>
 	<string name="app_description">FLOSS public transport passenger companion; a timetable in your pocket.</string>
-	<string name="website_button_description">" Bimba logo created by https://github.com/tebriz159"</string>
+	<string name="website_button_description">link to website</string>
 	<string name="code_button_description">link to source code</string>
-	<string name="mastodon_button_description">" Material icons © Google, Apache-2.0"</string>
+	<string name="mastodon_button_description">link to Mastodon</string>
 	<string name="use_online_feed">Use online feed</string>
-	<string name="information_may_be_outdated">" Map data © OpenStreetMap contributors (https://www.openstreetmap.org/copyright), ODbL-1.0"</string>
+	<string name="information_may_be_outdated">Information may be outdated</string>
 	<string name="current_timetable_validity">Current timetable valid: %1$s to %2$s</string>
-	<string name="error_406">" Cities list used for geocoding short plus codes © Geonames (https://geonames.org), CC BY"</string>
+	<string name="error_406">App version is not compatible with the server</string>
 	<string name="filter_localities">filter localities</string>
-	<string name="error_41">" Matrix logo ™/® Matrix.org"</string>
+	<string name="error_41">This locality is not supported by the server</string>
 	<string name="stop_from_qr_code">QR code stop</string>
 	<string name="title_select_date">Select day of departures</string>
 	<string name="title_select_line">Select line</string>




diff --git a/fruchtfleisch/build.gradle.kts b/fruchtfleisch/build.gradle.kts
index d8428614c918943ac054616ce6052d6d5a72fc33..51c8ff7f1f4c9ef0671ad822b9866310af7dbbb7 100644
--- a/fruchtfleisch/build.gradle.kts
+++ b/fruchtfleisch/build.gradle.kts
@@ -15,8 +15,8 @@     //implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.10")
 }
 
 java {
-    sourceCompatibility = JavaVersion.VERSION_17
-    targetCompatibility = JavaVersion.VERSION_17
+    sourceCompatibility = JavaVersion.VERSION_21
+    targetCompatibility = JavaVersion.VERSION_21
 }
 
 tasks.withType<Test> {