Bimba.git

commit 64b6ac71a98b9598b34df27988fa1932bf72b9a6

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

merge develop into master for version 3.7.0

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


diff --git a/.reuse/dep5 b/.reuse/dep5
deleted file mode 100644
index 0ec31152e0edaf81a99d37ff133cc698da9f669c..0000000000000000000000000000000000000000
--- a/.reuse/dep5
+++ /dev/null
@@ -1,33 +0,0 @@
-Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
-Upstream-Name: Bimba
-Upstream-Contact: Adam Evyčędo <bimba@apiote.xyz>
-Source: https://bimba.apiote.xyz
-
-Files: metadata/*
-Copyright: 2024 Adam Evyčędo <bimba@apiote.xyz>
-License: GPL-3.0-or-later
-
-Files: app/src/main/res/font/yellowcircle8.otf
-Copyright: 2024 Adam Evyčędo <bimba@apiote.xyz>
-License: OFL-1.1
-
-Files: app/src/main/res/mipmap-hdpi/*
-Copyright: 2024 Adam Evyčędo <bimba@apiote.xyz>
-License: GPL-3.0-or-later
-
-Files: app/src/main/res/mipmap-mdpi/*
-Copyright: 2024 Adam Evyčędo <bimba@apiote.xyz>
-License: GPL-3.0-or-later
-
-Files: app/src/main/res/mipmap-xhdpi/*
-Copyright: 2024 Adam Evyčędo <bimba@apiote.xyz>
-License: GPL-3.0-or-later
-
-Files: app/src/main/res/mipmap-xxhdpi/*
-Copyright: 2024 Adam Evyčędo <bimba@apiote.xyz>
-License: GPL-3.0-or-later
-
-Files: app/src/main/res/mipmap-xxxhdpi/*
-Copyright: 2024 Adam Evyčędo <bimba@apiote.xyz>
-License: GPL-3.0-or-later
-




diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index eaf4987492797d556a095a211eee2fbebc873db6..752ca17fa23bfb6f4fe112ab5b255bf899742ada 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -18,6 +18,16 @@ == [3.6.1] – 2024-09-04
 
 === Fixed
 
+* phantom feed names in search results
+
+=== Added
+
+* stops and departures from Transitous
+
+== [3.6.1] – 2024-09-04
+
+=== Fixed
+
 * add empty default cache credentials
 
 == [3.6] – 2024-08-30




diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0e259d42c996742e9e3cba14c677129b2c1b6311
--- /dev/null
+++ b/LICENSES/CC0-1.0.txt
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.




diff --git a/README.adoc b/README.adoc
index b484f6d8db8a2df8d8fad83be524e87818df188e..0e385ff83cfed168c84862a4776cf1669e2959ad 100644
--- a/README.adoc
+++ b/README.adoc
@@ -4,7 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 = Bimba
 Adam Evyčędo <me@apiote.xyz>
-v3.6.1 2024-09-04
+v3.7.0 2024-10-15
 :toc:
 
 Bimba is a FLOSS public transport passenger companion; a timetable in your pocket.




diff --git a/REUSE.toml b/REUSE.toml
new file mode 100644
index 0000000000000000000000000000000000000000..5fc28d21bbdaa5e70044fd318072dd332d0fc987
--- /dev/null
+++ b/REUSE.toml
@@ -0,0 +1,50 @@
+# SPDX-FileCopyrightText: Adam Evyčędo
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+version = 1
+SPDX-PackageName = "Bimba"
+SPDX-PackageSupplier = "Adam Evyčędo <bimba@apiote.xyz>"
+SPDX-PackageDownloadLocation = "https://bimba.apiote.xyz"
+
+[[annotations]]
+path = "metadata/**"
+precedence = "aggregate"
+SPDX-FileCopyrightText = "2024 Adam Evyčędo <bimba@apiote.xyz>"
+SPDX-License-Identifier = "GPL-3.0-or-later"
+
+[[annotations]]
+path = "app/src/main/res/font/yellowcircle8.otf"
+precedence = "aggregate"
+SPDX-FileCopyrightText = "2024 Adam Evyčędo <bimba@apiote.xyz>"
+SPDX-License-Identifier = "OFL-1.1"
+
+[[annotations]]
+path = "app/src/main/res/mipmap-hdpi/**"
+precedence = "aggregate"
+SPDX-FileCopyrightText = "2024 Adam Evyčędo <bimba@apiote.xyz>"
+SPDX-License-Identifier = "GPL-3.0-or-later"
+
+[[annotations]]
+path = "app/src/main/res/mipmap-mdpi/**"
+precedence = "aggregate"
+SPDX-FileCopyrightText = "2024 Adam Evyčędo <bimba@apiote.xyz>"
+SPDX-License-Identifier = "GPL-3.0-or-later"
+
+[[annotations]]
+path = "app/src/main/res/mipmap-xhdpi/**"
+precedence = "aggregate"
+SPDX-FileCopyrightText = "2024 Adam Evyčędo <bimba@apiote.xyz>"
+SPDX-License-Identifier = "GPL-3.0-or-later"
+
+[[annotations]]
+path = "app/src/main/res/mipmap-xxhdpi/**"
+precedence = "aggregate"
+SPDX-FileCopyrightText = "2024 Adam Evyčędo <bimba@apiote.xyz>"
+SPDX-License-Identifier = "GPL-3.0-or-later"
+
+[[annotations]]
+path = "app/src/main/res/mipmap-xxxhdpi/**"
+precedence = "aggregate"
+SPDX-FileCopyrightText = "2024 Adam Evyčędo <bimba@apiote.xyz>"
+SPDX-License-Identifier = "GPL-3.0-or-later"




diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 91096abf593d7294f7fb9a4c254b0b069b7e539e..2048280d5c542d1f7157d718aa9dc8d64c8fb584 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -19,8 +19,8 @@ 	defaultConfig {
 		applicationId = "xyz.apiote.bimba.czwek"
 		minSdk = 21
 		targetSdk = 35
-		versionCode = 31
-		versionName = "3.6.1"
+		versionCode = 32
+		versionName = "3.7.0"
 
 		testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
 		resourceConfigurations += listOf("en", "pl", "it", "de", "fr", "en-rUS")
@@ -54,30 +54,30 @@ 	implementation("androidx.core:core-ktx:1.13.1")
 	implementation("androidx.appcompat:appcompat:1.7.0")
 	implementation("com.google.android.material:material:1.12.0")
 	implementation("androidx.constraintlayout:constraintlayout:2.1.4")
-	implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.4")
-	implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4")
-	implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
-	implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
+	implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.6")
+	implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6")
+	implementation("androidx.navigation:navigation-fragment-ktx:2.8.2")
+	implementation("androidx.navigation:navigation-ui-ktx:2.8.2")
 	implementation("androidx.legacy:legacy-support-v4:1.0.0")
 	implementation("androidx.core:core-splashscreen:1.0.1")
 	implementation("com.google.openlocationcode:openlocationcode:1.0.4")
-	implementation("org.osmdroid:osmdroid-android:6.1.18")
-	implementation("org.yaml:snakeyaml:2.2")
-	implementation("androidx.activity:activity-ktx:1.9.1")
+	implementation("org.osmdroid:osmdroid-android:6.1.20")
+	implementation("org.yaml:snakeyaml:2.3")
+	implementation("androidx.activity:activity-ktx:1.9.2")
 	implementation("com.otaliastudios:zoomlayout:1.9.0")
 	implementation("dev.bandb.graphview:graphview:0.8.1")
-	implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1")
+	implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3")
 	implementation("com.github.jershell:kbson:0.5.0")
 	implementation("androidx.preference:preference-ktx:1.2.1")
 	implementation("androidx.work:work-runtime-ktx:2.9.1")
 	implementation("com.github.doyaaaaaken:kotlin-csv-jvm:1.10.0")
-	implementation("commons-io:commons-io:2.16.1")
-	implementation("com.google.guava:guava:31.0.1-android")
+	implementation("commons-io:commons-io:2.17.0")
+	implementation("com.google.guava:guava:33.3.1-android")
 	implementation(project(":fruchtfleisch"))
-	implementation("ch.acra:acra-http:5.11.3")
-	implementation("ch.acra:acra-notification:5.11.3")
+	implementation("ch.acra:acra-http:5.11.4")
+	implementation("ch.acra:acra-notification:5.11.4")
 
-	coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
+	coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.2")
 
 	testImplementation("junit:junit:4.13.2")
 	androidTestImplementation("androidx.test.ext:junit:1.2.1")




diff --git a/app/src/androidTest/java/xyz/apiote/bimba/czwek/units/TGMKtTest.kt b/app/src/androidTest/java/xyz/apiote/bimba/czwek/units/TGMKtTest.kt
index 8699c205595e80d6dca7922994585004ad8f3a31..4a2c9992ce3cee112261c8092a7f7c074a8bdc71 100644
--- a/app/src/androidTest/java/xyz/apiote/bimba/czwek/units/TGMKtTest.kt
+++ b/app/src/androidTest/java/xyz/apiote/bimba/czwek/units/TGMKtTest.kt
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
 package xyz.apiote.bimba.czwek.units
 
 import androidx.test.platform.app.InstrumentationRegistry




diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 66a021e365d8b0f0506f6a62a283ce3d9d891df6..a841a0098342acf33be8ce9c99c169d9d4536bd5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,4 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
+
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:tool="http://schemas.android.com/tools">
 




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/RoundedBackgroundSpan.kt b/app/src/main/java/xyz/apiote/bimba/czwek/RoundedBackgroundSpan.kt
index 13a93d42c47db30aa1a50a5bc2fbdfdd2ac8e3ff..de62227e174ece207c25db47a323c7f22bc7a863 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/RoundedBackgroundSpan.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/RoundedBackgroundSpan.kt
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
 package xyz.apiote.bimba.czwek
 
 import android.graphics.Canvas




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt
index ce68b7ac246599433119ef2f5aef9f8daf9c0cde..bdb383c7bb383976507ceeed0a8db083413fef7f 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt
@@ -208,7 +208,7 @@ 		}
 	}
 }
 
-private fun isNetworkAvailable(context: Context): Boolean {
+internal fun isNetworkAvailable(context: Context): Boolean {
 	val connectivityManager =
 		context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
 




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
new file mode 100644
index 0000000000000000000000000000000000000000..e566210b9774cd999d009f32c9b501433dfdf403
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitous.kt
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package xyz.apiote.bimba.czwek.api
+
+import android.content.Context
+import android.os.Build
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import xyz.apiote.bimba.czwek.R
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.URL
+
+suspend fun askTransitous(context: Context, json: String): Result {
+	return 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))
+		}
+	}
+}




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
new file mode 100644
index 0000000000000000000000000000000000000000..66fa6ae249f756a186b7405f5794b29726efd1db
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt
@@ -0,0 +1,281 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package xyz.apiote.bimba.czwek.api
+
+import android.content.Context
+import android.util.JsonReader
+import android.util.JsonToken
+import android.util.Log
+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.Alert
+import xyz.apiote.bimba.czwek.repo.Colour
+import xyz.apiote.bimba.czwek.repo.CongestionLevel
+import xyz.apiote.bimba.czwek.repo.Departure
+import xyz.apiote.bimba.czwek.repo.LineStub
+import xyz.apiote.bimba.czwek.repo.LineType
+import xyz.apiote.bimba.czwek.repo.OccupancyStatus
+import xyz.apiote.bimba.czwek.repo.Position
+import xyz.apiote.bimba.czwek.repo.Stop
+import xyz.apiote.bimba.czwek.repo.StopDepartures
+import xyz.apiote.bimba.czwek.repo.TrafficResponseException
+import xyz.apiote.bimba.czwek.repo.Vehicle
+import xyz.apiote.bimba.czwek.units.Mps
+import java.security.MessageDigest
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+import java.time.ZonedDateTime
+
+@OptIn(ExperimentalStdlibApi::class)
+suspend fun getTransitousDepartures(
+	context: Context,
+	stop: String,
+	date: LocalDate?,
+	limit: Int?
+): StopDepartures {
+	if (!isNetworkAvailable(context)) {
+		throw TrafficResponseException(0, "", Error(0, R.string.error_offline, R.drawable.error_net))
+	}
+
+	// TODO shouldn't it be start-of-day in stop's timezone?
+	val timestamp =
+		date?.atStartOfDay(ZoneId.systemDefault())?.toEpochSecond() ?: ZonedDateTime.now()
+			.toEpochSecond()
+
+	val json = """
+		{
+			"destination": {
+				"type": "Module",
+				"target": "/railviz/get_station"
+			},
+			"content_type": "RailVizStationRequest",
+			"content": {
+				"station_id": "$stop",
+				"time": $timestamp,
+				"event_count": ${limit ?: 12},
+				"direction": "LATER",
+				"by_schedule_time": false
+			}
+		}
+	"""
+
+	val result = askTransitous(context, json)
+
+	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 departures = mutableListOf<Departure>()
+			var stopID = ""
+			var stopName = ""
+			var latitude = 0.0
+			var longitude = 0.0
+			val r = JsonReader(result.stream!!.bufferedReader())
+			r.withObject { name ->
+				if (name != "content") {
+					r.skipValue()
+					return@withObject
+				}
+				r.withObject { name ->
+					when (name) {
+						"events" -> {
+							r.withArray {
+								var reason = ""
+								var eventTimestamp: Long = 0
+								var valid = false
+								var tripID = ""
+								var idLineID = ""
+								var stationID = ""
+								var targetStationID = ""
+								var targetTimestamp = 0
+								var idTimestamp = 0
+								var trainNr = 0
+								var clasz = -1
+								var direction = ""
+								var lineID = ""
+								var lineName = ""
+								var lineColour = ""
+								var eventType = ""
+								r.withObject { name ->
+									when (name) {
+										"event" -> {
+											r.withObject { name ->
+												when (name) {
+													"reason" -> reason = r.nextString()
+													"schedule_time" -> r.skipValue()
+													"schedule_track" -> r.skipValue()
+													"time" -> eventTimestamp = r.nextLong()
+													"track" -> r.skipValue()
+													"valid" -> valid = r.nextBoolean()
+													else -> r.skipValue()
+												}
+											}
+										}
+
+										"trips" -> {
+											r.withArray { i ->
+												if (i == 0) {
+													r.withObject { name ->
+														when (name) {
+															"id" -> {
+																r.withObject { name ->
+																	when (name) {
+																		"id" -> tripID = r.nextString()
+																		"line_id" -> idLineID = r.nextString()
+																		"station_id" -> stationID = r.nextString()
+																		"target_station_id" -> targetStationID = r.nextString()
+																		"target_time" -> targetTimestamp = r.nextInt()
+																		"time" -> idTimestamp = r.nextInt()
+																		"train_nr" -> trainNr = r.nextInt()
+																		else -> r.skipValue()
+																	}
+																}
+															}
+
+															"transport" -> {
+																r.withObject { name ->
+																	Log.d("WithObject", "next is ${name}")
+																	when (name) {
+																		"clasz" -> clasz = r.nextInt()
+																		"direction" -> direction = r.nextString()
+																		"line_id" -> lineID = r.nextString()
+																		"name" -> lineName = r.nextString()
+																		"provider" -> r.skipValue()
+																		"provider_url" -> r.skipValue()
+																		"range" -> r.skipValue()
+																		"route_color" -> lineColour = r.nextString()
+																		"route_text_color" -> r.skipValue()
+																		else -> r.skipValue()
+																	}
+																}
+															}
+															else -> r.skipValue()
+														}
+													}
+												} else {
+													r.skipValue()
+												}
+											}
+										}
+
+										"type" -> {
+											eventType = r.nextString()
+										}
+										else -> r.skipValue()
+									}
+								}
+								if (eventType == "ARR") {
+									val hash = MessageDigest.getInstance("SHA-256").let {
+										it.update(tripID.toByteArray())
+										it.update(idLineID.toByteArray())
+										it.update(stationID.toByteArray())
+										it.update(targetStationID.toByteArray())
+										it.update(targetTimestamp.toHexString().toByteArray())
+										it.update(idTimestamp.toHexString().toByteArray())
+										it.update(trainNr.toHexString().toByteArray())
+										it.digest()
+									}.toHexString()
+									val t =
+										ZonedDateTime.ofInstant(
+											Instant.ofEpochSecond(eventTimestamp),
+											ZoneId.systemDefault()
+										)
+									departures.add(
+										Departure(
+											ID = hash,
+											time = Time(
+												t.hour.toUInt(),
+												t.minute.toUInt(),
+												t.second.toUInt(),
+												(t.dayOfYear - ZonedDateTime.now().dayOfYear).toByte(),
+												ZoneId.systemDefault().id
+											),
+											status = 0u,
+											isRealtime = reason in setOf("IS", "PROPAGATION", "FORECAST"),
+											vehicle = Vehicle(
+												"",
+												Position(0.0, 0.0),
+												0u,
+												Mps(0),
+												LineStub(
+													lineName,
+													LineType.fromTransitous(clasz),
+													Colour.fromHex(lineColour)
+												),
+												direction,
+												CongestionLevel.UNKNOWN,
+												OccupancyStatus.UNKNOWN
+											),
+											boarding = 0xffu,
+											alerts = emptyList(),
+											exact = true,
+											terminusArrival = !valid
+										)
+									)
+								}
+							}
+						}
+
+						"station" -> {
+							r.withObject { name ->
+								when (name) {
+									"id" -> stopID = r.nextString()
+									"name" -> stopName = r.nextString()
+									"pos" -> {
+										r.withObject { posName ->
+											when (posName) {
+												"lat" -> latitude = r.nextDouble()
+												"lng" -> longitude = r.nextDouble()
+											}
+										}
+									}
+								}
+							}
+						}
+						else -> r.skipValue()
+					}
+				}
+			}
+
+			return@withContext StopDepartures(
+				departures,
+				Stop(stopID, stopName, stopName, "", "transitous", Position(latitude, longitude), listOf()),
+				listOf()
+			)
+		}
+	}
+}
+
+fun JsonReader.withObject(f: (String) -> Unit) {
+	beginObject()
+	while (hasNext()) {
+		try {
+			f(nextName())
+		} catch (e: IllegalStateException) {
+			val a: JsonToken = this.peek()
+			Log.d("WithObject", "next is ${a.name} ${nextString()}")
+			throw e
+		}
+	}
+	endObject()
+}
+
+fun JsonReader.withArray(f: (Int) -> Unit) {
+	beginArray()
+	var i = 0
+	while (hasNext()) {
+		f(i)
+		i++
+	}
+	endArray()
+}




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
new file mode 100644
index 0000000000000000000000000000000000000000..71d32872c65e42a1c27a8115ccf99bb3e4cdb819
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousQueryables.kt
@@ -0,0 +1,196 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package xyz.apiote.bimba.czwek.api
+
+import android.content.Context
+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 xyz.apiote.bimba.czwek.units.DistanceUnit
+import xyz.apiote.bimba.czwek.units.Metre
+
+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": 24
+			}
+		}
+	"""
+	val result = askTransitous(context, json)
+
+	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
+		}
+	}
+}
+
+suspend fun locateTransitousQueryables(position: Position, context: Context, radius: DistanceUnit = Metre(500.0)): List<Queryable> {
+	if (!isNetworkAvailable(context)) {
+		throw TrafficResponseException(0, "", Error(0, R.string.error_offline, R.drawable.error_net))
+	}
+
+	val json = """
+		{
+			"destination": {
+				"type": "Module",
+				"target": "/lookup/geo_station"
+			},
+			"content_type": "LookupGeoStationRequest",
+			"content": {
+				"max_radius": ${radius.meters().toInt()},
+				"pos": {
+					"lat": ${position.latitude},
+					"lng": ${position.longitude}
+				}
+			}
+		}
+	"""
+	val result = askTransitous(context, json)
+
+	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
+		}
+	}
+}




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
new file mode 100644
index 0000000000000000000000000000000000000000..b135a0c210d068dcde94ba25b825e54ded1ea350
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousSettings.kt
@@ -0,0 +1,12 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+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)
+}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
index 29ab95545f1d7ad0c79246c6a85e095b5f023af5..6aa86b0cb2362798e410558411e5229ae8ba873e 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt
@@ -74,6 +74,7 @@ 				)
 			}
 
 			content.findViewById<TextView>(R.id.time).visibility = View.GONE
+			content.findViewById<TextView>(R.id.local_time).visibility = View.GONE
 
 			content.findViewById<MapView>(R.id.map).visibility = View.GONE
 			content.findViewById<LinearLayout>(R.id.boarding).visibility = View.GONE




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
index 5f50f5add8aba3a517babe6d39eb0166eef25257..17c08de21676990de2632a77c932c099454c4626 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
@@ -34,6 +34,7 @@ import org.osmdroid.views.MapView
 import org.osmdroid.views.overlay.Marker
 import org.osmdroid.views.overlay.TilesOverlay
 import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
+import org.w3c.dom.Text
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.dpToPixelI
 import xyz.apiote.bimba.czwek.repo.Alert
@@ -179,7 +180,7 @@ 		PreferenceManager.getDefaultSharedPreferences(
 			it
 		).getString(TERMINUS_ARRIVAL_SHOWING_KEY, TERMINUS_ARRIVAL_GREY_OUT)
 	}
-	?: TERMINUS_ARRIVAL_GREY_OUT
+		?: TERMINUS_ARRIVAL_GREY_OUT
 
 	var lastUpdate: ZonedDateTime =
 		ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault())
@@ -204,15 +205,15 @@ 			val oldDeparture = oldDepartures[oldItemPosition]
 			val newDeparture = newDepartures[newItemPosition]
 			return if (oldDeparture.departure != null && newDeparture.departure != null) {
 				!oldDeparture.departure.terminusArrival &&
-				oldDeparture.departure.terminusArrival == newDeparture.departure.terminusArrival &&
-					oldDeparture.departure.exact == newDeparture.departure.exact &&
-					oldDeparture.departure.vehicle.Line == newDeparture.departure.vehicle.Line &&
-					oldDeparture.departure.vehicle.Headsign == newDeparture.departure.vehicle.Headsign &&
-					oldDeparture.departure.statusText(
-						context,
-						false,
-						lastUpdate
-					) == newDeparture.departure.statusText(context, false) && !showAsTimeChanged
+						oldDeparture.departure.terminusArrival == newDeparture.departure.terminusArrival &&
+						oldDeparture.departure.exact == newDeparture.departure.exact &&
+						oldDeparture.departure.vehicle.Line == newDeparture.departure.vehicle.Line &&
+						oldDeparture.departure.vehicle.Headsign == newDeparture.departure.vehicle.Headsign &&
+						oldDeparture.departure.statusText(
+							context,
+							false,
+							lastUpdate
+						) == newDeparture.departure.statusText(context, false) && !showAsTimeChanged
 			} else if (oldDeparture.alert.isNotEmpty() && newDeparture.alert.isEmpty()) {
 				oldDeparture.alert == newDeparture.alert
 			} else {
@@ -339,6 +340,12 @@
 	private fun setContent(view: View, ctx: Context, updating: Boolean = false) {
 		view.apply {
 			findViewById<TextView>(R.id.time).text = departure.timeString(ctx)
+			findViewById<TextView>(R.id.local_time).visibility =
+				if (departure.time.Zone == ZoneId.systemDefault().id) {
+					View.GONE
+				} else {
+					View.VISIBLE
+				}
 
 			findViewById<ImageView>(R.id.rt_icon).apply {
 				visibility = if (departure.isRealtime) {
@@ -372,7 +379,15 @@ 					departure.vehicle.Headsign
 				)
 			}
 
-			findViewById<TextView>(R.id.boarding_text).text = departure.boardingText(ctx)
+
+			departure.boardingText(ctx).let {
+				findViewById<TextView>(R.id.boarding_text).text = it
+					findViewById<ImageView>(R.id.boarding_icon).visibility = if (it == "") {
+						View.GONE
+					} else {
+						View.VISIBLE
+					}
+			}
 			UnitSystem.getSelected(requireContext()).let { us ->
 				findViewById<TextView>(R.id.speed_text).apply {
 					text =
@@ -522,7 +537,7 @@ 			content.apply {
 				findViewById<MapView>(R.id.map).let { map ->
 					map.setTileSource(TileSourceFactory.MAPNIK)
 					if (((context?.resources?.configuration?.uiMode ?: UI_MODE_NIGHT_UNDEFINED)
-							and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
+								and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
 					) {
 						map.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
 					}




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/Departure.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
index 66ac62094ec48793ce330634aa369da2d1b18c68..38a48a946c2048ad2fa19b5eb4fa169769287adc 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
@@ -232,6 +232,7 @@ 	fun boardingText(context: Context): String {
 		// todo [3.x] probably should take into account (on|off)-boarding only, on demand
 		return when {
 			boarding == (0b0000_0000).toUByte() -> context.getString(R.string.no_boarding)
+			boarding == (0b1111_1111).toUByte() -> "" // unknown
 			boarding.and(0b0011_0011u) == (0b0000_0001).toUByte() -> context.getString(R.string.on_boarding)
 			boarding.and(0b0011_0011u) == (0b0001_0000).toUByte() -> context.getString(R.string.off_boarding)
 			boarding.and(0b0011_0011u) == (0b0001_0001).toUByte() -> context.getString(R.string.boarding)




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Favourite.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Favourite.kt
index a24d42ebccfb157547772a58006dcfcb0f561d05..fa0a08da12719f5e4c70a44f5f002a995adbeb45 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Favourite.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Favourite.kt
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
 package xyz.apiote.bimba.czwek.repo
 
 data class Favourite(val sequence: Int?, val feedID: String, val feedName: String, val stopCode: String, val stopName: String, val lines: List<String>)
\ 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 1ba24e3cbe16562fe30e5fb186d1087c948659c4..89f7f8d5ccc84cde130f7c6a92ded75a16e30170 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,6 +50,7 @@ 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.units.Metre
 import java.time.LocalDate
 
 // todo [3.2] in Repository check if responses are BARE or HTML
@@ -82,7 +87,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)) }
@@ -90,6 +95,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
 		}
 	}
 
@@ -100,8 +115,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,
@@ -109,42 +126,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
+				}
 			}
 		}
 	}
@@ -154,56 +173,81 @@ 		context: Context,
 		bl: Position,
 		tr: Position,
 	): List<Locatable>? {
-		val result = xyz.apiote.bimba.czwek.api.getLocatablesIn(
-			context,
-			Server.get(context),
-			PositionV1(bl.latitude, bl.longitude),
-			PositionV1(tr.latitude, tr.longitude)
-		)
-		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)
-			}
+		val transitousQueryables = if (Server.get(context).feeds.transitousEnabled()) {
+			val centre = Position(
+				latitude = (bl.latitude + tr.latitude) / 2,
+				longitude = (bl.longitude + tr.longitude) / 2
+			)
+			locateTransitousQueryables(
+				centre, context, Metre(centre.distanceTo(bl).coerceAtMost(5000f).toDouble())
+			).map {
+				when (it) {
+					is Stop -> it
+					else -> null
+				}
+			}.filterNotNull()
 		} else {
-			return when (val response =
-				withContext(Dispatchers.IO) { LocatablesResponse.unmarshal(result.stream!!) }) {
-				is LocatablesResponseDev -> response.locatables.map {
-					when (it) {
-						is StopV3 -> Stop(it)
-						is VehicleV3 -> Vehicle(it)
-						else -> throw UnknownResourceException("locatables", it::class)
-					}
+			null
+		}
+		val bimbaQueryables = if (Server.get(context).feeds.bimbaEnabled()) {
+			val result = xyz.apiote.bimba.czwek.api.getLocatablesIn(
+				context,
+				Server.get(context),
+				PositionV1(bl.latitude, bl.longitude),
+				PositionV1(tr.latitude, tr.longitude)
+			)
+			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 {
+				when (val response =
+					withContext(Dispatchers.IO) { LocatablesResponse.unmarshal(result.stream!!) }) {
+					is LocatablesResponseDev -> response.locatables.map {
+						when (it) {
+							is StopV3 -> Stop(it)
+							is VehicleV3 -> Vehicle(it)
+							else -> throw UnknownResourceException("locatables", it::class)
+						}
+					}
 
-				is LocatablesResponseV3 -> response.locatables.map {
-					when (it) {
-						is StopV2 -> Stop(it)
-						is VehicleV3 -> Vehicle(it)
-						else -> throw UnknownResourceException("locatables", it::class)
+					is LocatablesResponseV3 -> response.locatables.map {
+						when (it) {
+							is StopV2 -> Stop(it)
+							is VehicleV3 -> Vehicle(it)
+							else -> throw UnknownResourceException("locatables", it::class)
+						}
 					}
-				}
 
-				is LocatablesResponseV2 -> response.locatables.map {
-					when (it) {
-						is StopV2 -> Stop(it)
-						is VehicleV2 -> Vehicle(it)
-						else -> throw UnknownResourceException("locatables", it::class)
+					is LocatablesResponseV2 -> response.locatables.map {
+						when (it) {
+							is StopV2 -> Stop(it)
+							is VehicleV2 -> Vehicle(it)
+							else -> throw UnknownResourceException("locatables", it::class)
+						}
 					}
-				}
 
-				is LocatablesResponseV1 -> response.locatables.map {
-					when (it) {
-						is StopV1 -> Stop(it)
-						is VehicleV1 -> Vehicle(it)
-						else -> throw UnknownResourceException("locatables", it::class)
+					is LocatablesResponseV1 -> response.locatables.map {
+						when (it) {
+							is StopV1 -> Stop(it)
+							is VehicleV1 -> Vehicle(it)
+							else -> throw UnknownResourceException("locatables", it::class)
+						}
 					}
-				}
 
-				else -> null
+					else -> null
+				}
 			}
+		} else {
+			null
+		}
+		return if (transitousQueryables == null && bimbaQueryables == null) {
+			null
+		} else {
+			(bimbaQueryables ?: listOf()) + (transitousQueryables ?: listOf())
 		}
 	}
 
@@ -234,13 +278,43 @@
 	override suspend fun queryQueryables(
 		query: String, context: Context
 	): List<Queryable>? {
-		return getQueryables(query, null, context, "query")
+		val transitousQueryables = if (Server.get(context).feeds.transitousEnabled()) {
+			getTransitousQueryables(query, context)
+		} else {
+			null
+		}
+		val bimbaQueryables = if (Server.get(context).feeds.bimbaEnabled()) {
+			getQueryables(query, null, context, "query")
+		} else {
+			null
+		}
+		return if (transitousQueryables == null && bimbaQueryables == null) {
+			null
+		} else {
+			(bimbaQueryables ?: listOf()) + (transitousQueryables ?: listOf())
+		}
 	}
 
 	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..6edad804085f30265578b89d4abe728cf3c147c8 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,9 +139,11 @@ 				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 ?: ""
+			} else {
+				holder?.feedName?.visibility = View.GONE
 			}
 			context?.let {
 				stop.changeOptions(it, Stop.LineDecoration.fromPreferences(context)).let { changeOptions ->




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt
index 7fd75e381c417517d76ecd5d8b2f68ac68718d0a..2eedfa8ae0b4b12f54659981d15aac8f1ce531a8 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
 package xyz.apiote.bimba.czwek.search
 
 import android.content.Context




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt
index feb3d2b4e863968a3bd9715c5990bba505205e6f..659958c825ea9c7190bf7ae9e42390cbacac2f06 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
 package xyz.apiote.bimba.czwek.settings
 
 import android.Manifest




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt
index 7f75a263c06670675535a7945c4367d2e62a30a1..574631882827027aab7db9264424a46b9f5ae01a 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
 package xyz.apiote.bimba.czwek.settings
 
 import android.content.Context




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/java/xyz/apiote/bimba/czwek/units/TGM.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/TGM.kt
index d99b72fac627db1e05c134ee36112e0a1d2ac798..e08dd2695c1885cb85355d300f2445554a184882 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/units/TGM.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/TGM.kt
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
 package xyz.apiote.bimba.czwek.units
 
 import android.content.Context




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/UnitSystem.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/UnitSystem.kt
index 9d0e88c75fc8e29c60f42e938836d8aea33f5b09..3e3f5045e23857de6a6155aceb3a681b7cc66ec3 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/units/UnitSystem.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/UnitSystem.kt
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
 package xyz.apiote.bimba.czwek.units
 
 import android.content.Context




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/imperial.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/imperial.kt
index f06ce367fd44750b68a9f8eb3e1a60835d232898..5938c9e483126a316bf0f75ded2db2892d9ab77f 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/units/imperial.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/imperial.kt
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
 package xyz.apiote.bimba.czwek.units
 
 import android.content.Context




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/metric.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/metric.kt
index 2f410632b40b89c76a02ad9d6e1f33097c15970e..9ba9250282b2709ea0712489ccf6919f9592ea19 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/units/metric.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/metric.kt
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
 package xyz.apiote.bimba.czwek.units
 
 import android.content.Context




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/units/usCustomary.kt b/app/src/main/java/xyz/apiote/bimba/czwek/units/usCustomary.kt
index eb5013c9735a7166f5f822311799e00b4beb072d..b9228f6fd2577b0d9c3eca067940b40e09a639bb 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/units/usCustomary.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/units/usCustomary.kt
@@ -1,3 +1,7 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
 package xyz.apiote.bimba.czwek.units
 
 import android.content.Context




diff --git a/app/src/main/res/drawable/matrix.xml b/app/src/main/res/drawable/matrix.xml
index 54644fae698519a4c776de8dd92c0fd1e3e09053..2e289c80ee9ad890fbadaa99833a0c6de6ad5ecb 100644
--- a/app/src/main/res/drawable/matrix.xml
+++ b/app/src/main/res/drawable/matrix.xml
@@ -1,3 +1,9 @@
+<!--
+SPDX-FileCopyrightText: Matrix Foundation
+
+SPDX-License-Identifier: CC0-1.0
+-->
+
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     android:width="48dp"
     android:height="48dp"




diff --git a/app/src/main/res/drawable/plane_black.xml b/app/src/main/res/drawable/plane_black.xml
new file mode 100644
index 0000000000000000000000000000000000000000..57d6132a2c3a543e412d8cda68b03fe36080fd5d
--- /dev/null
+++ b/app/src/main/res/drawable/plane_black.xml
@@ -0,0 +1,11 @@
+<!--
+SPDX-FileCopyrightText: Google
+
+SPDX-License-Identifier: Apache-2.0
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M22,16v-2l-8.5,-5V3.5C13.5,2.67 12.83,2 12,2s-1.5,0.67 -1.5,1.5V9L2,14v2l8.5,-2.5V19L8,20.5L8,22l4,-1l4,1l0,-1.5L13.5,19v-5.5L22,16z"/>
+    
+</vector>




diff --git a/app/src/main/res/layout/alert.xml b/app/src/main/res/layout/alert.xml
index 56819d0fd9f418fd6c0e896db130906251530f78..a1488e4efc5bd06fdb4e6d8f23f1636b9ad07e0a 100644
--- a/app/src/main/res/layout/alert.xml
+++ b/app/src/main/res/layout/alert.xml
@@ -1,4 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
+
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
 <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
 	xmlns:tool="http://schemas.android.com/tools"




diff --git a/app/src/main/res/layout/departure_bottom_sheet.xml b/app/src/main/res/layout/departure_bottom_sheet.xml
index 5d5c7b3acb6ae7a0b5a21c7c50f67771ed7fc176..3b6e92fa314f2d9967ae05c9200666b9989f8bbb 100644
--- a/app/src/main/res/layout/departure_bottom_sheet.xml
+++ b/app/src/main/res/layout/departure_bottom_sheet.xml
@@ -31,6 +31,16 @@ 		app:layout_constraintTop_toBottomOf="@+id/drag_handle"
 		tool:text="at 12:10:30" />
 
 	<com.google.android.material.textview.MaterialTextView
+		android:id="@+id/local_time"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		app:layout_constraintTop_toBottomOf="@+id/time"
+		app:layout_constraintEnd_toEndOf="@+id/time"
+		android:text="@string/local_time"
+		android:visibility="gone"
+		app:layout_constraintStart_toStartOf="@+id/time" />
+
+	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/offset"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
@@ -73,7 +83,7 @@ 		android:textAlignment="center"
 		android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@id/time" />
+		app:layout_constraintTop_toBottomOf="@id/local_time" />
 
 	<androidx.constraintlayout.helper.widget.Flow
 		android:id="@+id/info"




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/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml
index b5b5089cad3cc4480dae8e346bc72ab4d18096c9..d214c5dac728ebb9433d8cc381641595ffa3c6fe 100644
--- a/app/src/main/res/layout/settings_activity.xml
+++ b/app/src/main/res/layout/settings_activity.xml
@@ -1,3 +1,9 @@
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent">




diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index cf156aef73eedc2bde7a2d851b0b81fcf8842ac1..c710126e20b4f019657300ecb9aa147f18ad8b19 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -1,3 +1,9 @@
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
 <resources>
 	<string-array name="unit_entries">
 		<item>@string/units_locale_based</item>




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0f28969a1ce9661561811d143e015aedfe141d66..f0881e76ecbc33476956161aa61f7919fde2a3d0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -285,4 +285,7 @@ 	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>
+	<string name="local_time">local time</string>
 </resources>




diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 23ea65341109cbdcd89d71536547c76ec2212e18..7749d176db5065632765b6c4d761bdacdad151b5 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -186,4 +186,6 @@     drei
     <string name="four">vier</string>
     <string name="five">fünf</string>
     <string name="seven">sieben</string>
+    <string name="send">Senden</string>
+    <string name="discard">Verwerfen</string>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml
index ed15c05948b2e42e983638a24f3c885597f30c96..e39bfc51ee91e735e1e5a29982850c5b76cfddd5 100644
--- a/app/src/main/res/values-en-rGB/strings.xml
+++ b/app/src/main/res/values-en-rGB/strings.xml
@@ -283,4 +283,7 @@ 	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>
+	<string name="local_time">local time</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..e41b22e601cc5d006112aa47cb3f0a9ea341921a 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>
@@ -280,4 +280,7 @@ 	Terminus arrivals
 	<string name="matrix_button_description">link to Matrix channel</string>
 	<string name="email_button_description">link to email</string>
 	<string name="no_email_app">No email app installed</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>
+	<string name="local_time">local time</string>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 1a44d63c4dfbb2e513512abbbe81adabb33a2ef4..f229828d8dc9371528c4c32a6f7ab654b2860eec 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -102,15 +102,7 @@     Filtrer par ligne
     <string name="title_filter_bytime">Filtrer par horaire</string>
     <string name="title_select_time_end">Sélectionner l\'heure d\'arrivée</string>
     <string name="title_select_time_start">Sélectionner l\'heure de départ</string>
-    <string name="credits">Police yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) basée sur Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans
-\n
-\n Icône Mastodon (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later
-\n
-\n Logo Bimba créé par https://github.com/tebriz159
-\n
-\n Icônes Material © Google, Apache-2.0
-\n
-\n Données cartographiques © OpenStreetMap contributors, ODbL-1.0</string>
+    <string name="credits">Police yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) basée sur Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n Icône Mastodon (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Logo Bimba créé par https://github.com/tebriz159\n\n Icônes Material © Google, Apache-2.0\n\n Données cartographiques © OpenStreetMap contributors, ODbL-1.0</string>
     <string name="error_400">L\'application a effectué une requête mal formulée</string>
     <string name="error_404">Pas trouvé</string>
     <string name="error_429">Limite dépassée. Réessayez plus tard</string>




diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 9b4c97506c9d76ed958272f6fa3adac7d6357eb4..0eafe8213914c69435cc0f085145d1a8db9c8a13 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -109,17 +109,7 @@     Orario attuale valido: %1$s fino alla %2$s
     <string name="error_406">La versione dell’app non è compatibile con il server</string>
     <string name="filter_localities">filtra le località</string>
     <string name="error_41">Questa località non è supportata dal server</string>
-    <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) basato su Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans
-\n
-\n L’icona di Mastodon (https://github.com/mastodon/joinmastodon) © collaboratori Mastodon, AGPL-3.0-or-later
-\n
-\n L’icona di Bimba creata da https://github.com/tebriz159
-\n
-\n Material icons © Google, Apache-2.0
-\n
-\n Dati della carta © collaboratori OpenStreetMap (https://www.openstreetmap.org/copyright), ODbL-1.0
-\n
-\nElenco delle città utilizzato per la geocodificazione dei brevi codici OLC © Geonames (https://geonames.org), CC BY</string>
+    <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) basato su Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n L’icona di Mastodon (https://github.com/mastodon/joinmastodon) © collaboratori Mastodon, AGPL-3.0-or-later\n\n L’icona di Bimba creata da https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Dati della carta © collaboratori OpenStreetMap (https://www.openstreetmap.org/copyright), ODbL-1.0\n\nElenco delle città utilizzato per la geocodificazione dei brevi codici OLC © Geonames (https://geonames.org), CC BY</string>
     <string name="stop_from_qr_code">Stop del codice QR</string>
     <string name="title_select_date">Scegli il giorno della partenza</string>
     <string name="title_select_line">Scegli la linea</string>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 6879010dd16f035617f2930f61b1a6a86163abed..a583c09cc1089d2f2f972a9b0596380c0a308b7e 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -100,17 +100,7 @@     Uprawnienia do używania lokalizacji są wymagane, aby znaleźć przystanki w pobliżu i pokazać aktualną pozycję na mapie. Pozostałe funkcje będą działały bez tych uprawnień. Mogą być one w każdym momencie nadane i odebrane w ustawieniach systemowych.
     <string name="stop_stub_on_demand_in_zone">Przystanek na żądanie w strefie %1$s</string>
     <string name="stop_stub_on_demand">Przystanek na żądanie</string>
     <string name="stop_stub_in_zone">Przystanek w strefie %1$s</string>
-    <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) na podstawie Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans
-\n
-\n Ikona Mastodona (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later
-\n
-\n Logo Bimby stworzone przez https://github.com/tebriz159
-\n
-\n Material icons © Google, Apache-2.0
-\n
-\n Dane mapy © kontrybutorzy OpenStreetMap (https://www.openstreetmap.org/copyright), ODbL-1.0
-\n
-\n Lista miast użyta do geokodowania krótkich kodów OLC © Geonames (https://geonames.org), CC BY</string>
+    <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) na podstawie Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n Ikona Mastodona (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Logo Bimby stworzone przez https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Dane mapy © kontrybutorzy OpenStreetMap (https://www.openstreetmap.org/copyright), ODbL-1.0\n\n Lista miast użyta do geokodowania krótkich kodów OLC © Geonames (https://geonames.org), CC BY</string>
     <string name="title_about">O Bimbie</string>
     <string name="translation_button_description">link do narzędzia do tłumaczeń</string>
     <string name="app_description">Wolny i otwarty kompan pasażerów transportu publicznego; rozkład jazdy w twojej kieszeni.</string>
@@ -290,7 +280,7 @@         %1$s wlosy
         <item quantity="many">%1$s wlosów</item>
         <item quantity="other">%1$s wlosów</item>
     </plurals>
-	<string name="none">brak</string>
+    <string name="none">brak</string>
     <string name="italics">kursywa</string>
     <string name="colour">kolor</string>
     <string name="line_decorations">Dekoracje nazw linii</string>
@@ -311,4 +301,6 @@     Przyjazdy na pętle
     <string name="matrix_button_description">link do kanału na Matrixie</string>
     <string name="email_button_description">link do e-maila</string>
     <string name="no_email_app">Brak aplikacji e-mail</string>
+    <string name="transitous_attribution">API Transitous (https://transitous.org) dostarczane przez Spline (https://routing.spline.de). Lokalizacje (https://github.com/public-transport/transitous/tree/main/feeds) utrzymywane przez społeczność.</string>
+    <string name="local_time">czas lokalny</string>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
index 27e57b55cc383ece2b9d21fa54bca4717eee18c2..52ded369d35deb8e357f2bba5c042a0adc2de048 100644
--- a/app/src/main/res/xml/locales_config.xml
+++ b/app/src/main/res/xml/locales_config.xml
@@ -1,4 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
+
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
 <locale-config xmlns:android="http://schemas.android.com/apk/res/android">
     <locale android:name="en"/>
     <locale android:name="pl"/>




diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index 2127ae46dc0a24e21c53e4cc6fcd662fe492916e..47230fb1e059c9c12699e5d229b2b65e3182d971 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -1,3 +1,9 @@
+<!--
+SPDX-FileCopyrightText: Adam Evyčędo
+
+SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
 <PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
 	<PreferenceCategory app:title="Units">
 	<ListPreference




diff --git a/bimba.svg b/bimba.svg
index 8bfa5182d6af0ad1a4cfa32f9383bcb8c1844c9a..42fb98d50eb985b418012cbfa8a92189e645c834 100644
--- a/bimba.svg
+++ b/bimba.svg
@@ -4,14 +4,15 @@ SPDX-FileCopyrightText: https://github.com/tebriz159
 
 SPDX-License-Identifier: GPL-3.0-or-later
 -->
+
 <svg
-   viewBox="0 0 91.999998 92"
+   viewBox="0 0 47.999999 48"
    id="vector"
    version="1.1"
    sodipodi:docname="bimba.svg"
-   width="92"
-   height="92"
-   inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
+   width="48"
+   height="48"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
    inkscape:export-filename="bimba.png"
    inkscape:export-xdpi="534.26086"
    inkscape:export-ydpi="534.26086"
@@ -48,80 +49,83 @@      inkscape:window-width="1504"
      inkscape:window-height="1002"
      id="namedview172"
      showgrid="false"
-     fit-margin-top="8"
-     fit-margin-left="8"
-     fit-margin-bottom="8"
-     fit-margin-right="8"
-     inkscape:zoom="8.5108696"
-     inkscape:cx="46"
-     inkscape:cy="-26.965517"
-     inkscape:window-x="0"
-     inkscape:window-y="0"
+     fit-margin-top="4"
+     fit-margin-left="4"
+     fit-margin-bottom="4"
+     fit-margin-right="4"
+     inkscape:zoom="8.05"
+     inkscape:cx="26.459627"
+     inkscape:cy="19.006211"
+     inkscape:window-x="56"
+     inkscape:window-y="50"
      inkscape:window-maximized="0"
      inkscape:current-layer="vector"
      inkscape:showpageshadow="0"
      inkscape:pagecheckerboard="0"
      inkscape:deskcolor="#505050" />
-  <rect
-     id="rect178"
-     width="92"
-     height="92"
-     x="0"
-     y="0"
-     style="display:inline;fill:#3a3a3b;fill-opacity:1;stroke-width:0.436372"
-     ry="0" />
   <g
-     id="group_1"
-     transform="matrix(0.16904335,0,0,0.16904335,-7.9999769,-8)"
-     style="display:inline">
+     id="g1252"
+     transform="matrix(0.43478261,0,0,0.43478261,4,4)">
+    <path
+       fill="none"
+       stroke="#000000"
+       stroke-width="0.645279"
+       d="M 46,92 C 66.156946,92 77.36246,92 84.681245,84.681247 92,77.362462 92,66.15695 92,46.000001 92,25.843058 92,14.637536 84.681245,7.3187613 77.36246,0 66.156946,0 46,0 25.843059,0 14.637536,0 7.3187622,7.3187613 0,14.637536 0,25.843058 0,46.000001 0,66.15695 0,77.362462 7.3187622,84.681247 14.637536,92 25.843059,92 46,92 Z"
+       id="path922"
+       style="fill:#3a3a3b;fill-opacity:1;stroke:none" />
     <g
-       id="group"
-       transform="translate(173.03255,173.03255)">
-      <path
-         id="path"
-         d="m 62.962,292.5 v 0 c -3.7,-0.9 -5.7,-3.8 -4.3,-6.5 l 32.3,-62.9 c 1.3,-2.6 5.5,-4 9.2,-3 v 0 c 3.7,0.9 5.7,3.8 4.3,6.5 l -32.3,62.9 c -1.3,2.6 -5.4,4 -9.2,3 z"
-         inkscape:connector-curvature="0"
-         style="fill:#54af39" />
-      <path
-         id="path_1"
-         d="m 229.662,292.5 v 0 c 3.7,-0.9 5.7,-3.8 4.3,-6.5 l -32.3,-62.9 c -1.3,-2.6 -5.5,-4 -9.2,-3 v 0 c -3.7,0.9 -5.7,3.8 -4.3,6.5 l 32.3,62.9 c 1.3,2.6 5.5,4 9.2,3 z"
-         inkscape:connector-curvature="0"
-         style="fill:#54af39" />
-      <path
-         id="path_2"
-         d="m 151.362,36.9 -0.6,0.2 c -1.8,0.7 -3.8,-0.3 -4.5,-2.1 l -9.6,-26.7 c -0.7,-1.8 0.3,-3.8 2.1,-4.5 l 0.6,-0.2 c 1.8,-0.7 3.8,0.3 4.5,2.1 l 9.6,26.7 c 0.7,1.9 -0.3,3.9 -2.1,4.5 z"
-         inkscape:connector-curvature="0"
-         style="fill:#54af39" />
-      <path
-         id="path_3"
-         d="m 180.662,3.8 v 0 c 0,2.1 -1.7,3.8 -3.8,3.8 h -61 c -2.1,0 -3.8,-1.7 -3.8,-3.8 v 0 c 0,-2.1 1.7,-3.8 3.8,-3.8 h 61 c 2.1,0 3.8,1.7 3.8,3.8 z"
-         inkscape:connector-curvature="0"
-         style="fill:#54af39" />
-      <path
-         id="path_4"
-         d="m 218.762,236.7 h -144.9 c -13.3,0 -24,-10.8 -24,-24 v -108 c 0,-41.3 33.5,-74.9 74.9,-74.9 h 43.3 c 41.3,0 74.9,33.5 74.9,74.9 v 108 c -0.1,13.2 -10.9,24 -24.2,24 z"
-         inkscape:connector-curvature="0"
-         style="fill:#54af39" />
-      <path
-         id="path_5"
-         d="m 212.562,146.2 h -132.5 c -5.6,0 -10.2,-4.5 -10.2,-10.2 v -34.9 c 0,-16.9 13.7,-30.6 30.6,-30.6 h 91.7 c 16.9,0 30.6,13.7 30.6,30.6 V 136 c -0.1,5.7 -4.6,10.2 -10.2,10.2 z"
-         inkscape:connector-curvature="0"
-         style="fill:#ffffff" />
-      <path
-         id="path_6"
-         d="m 161.462,55.5 h -30.3 c -3.2,0 -5.7,-2.6 -5.7,-5.7 v 0 c 0,-3.2 2.6,-5.7 5.7,-5.7 h 30.3 c 3.2,0 5.7,2.6 5.7,5.7 v 0 c 0,3.1 -2.6,5.7 -5.7,5.7 z"
-         inkscape:connector-curvature="0"
-         style="fill:#ffffff" />
-      <path
-         id="path_7"
-         d="m 87.062,191.9 m -14.8,0 c 0,-3.924 1.56,-7.691 4.335,-10.465 2.774,-2.775 6.541,-4.335 10.465,-4.335 3.924,0 7.691,1.56 10.465,4.335 2.775,2.774 4.335,6.541 4.335,10.465 0,3.924 -1.56,7.691 -4.335,10.465 -2.774,2.775 -6.541,4.335 -10.465,4.335 -3.924,0 -7.691,-1.56 -10.465,-4.335 -2.775,-2.774 -4.335,-6.541 -4.335,-10.465"
-         inkscape:connector-curvature="0"
-         style="fill:#ffffff" />
-      <path
-         id="path_8"
-         d="m 205.662,191.9 m -14.8,0 c 0,-3.924 1.56,-7.691 4.335,-10.465 2.774,-2.775 6.541,-4.335 10.465,-4.335 3.924,0 7.691,1.56 10.465,4.335 2.775,2.774 4.335,6.541 4.335,10.465 0,3.924 -1.56,7.691 -4.335,10.465 -2.774,2.775 -6.541,4.335 -10.465,4.335 -3.924,0 -7.691,-1.56 -10.465,-4.335 -2.775,-2.774 -4.335,-6.541 -4.335,-10.465"
-         inkscape:connector-curvature="0"
-         style="fill:#ffffff" />
+       id="group_1"
+       transform="matrix(0.16904335,0,0,0.16904335,-7.9999769,-8)"
+       style="display:inline">
+      <g
+         id="group"
+         transform="translate(173.03255,173.03255)">
+        <path
+           id="path"
+           d="m 62.962,292.5 v 0 c -3.7,-0.9 -5.7,-3.8 -4.3,-6.5 l 32.3,-62.9 c 1.3,-2.6 5.5,-4 9.2,-3 v 0 c 3.7,0.9 5.7,3.8 4.3,6.5 l -32.3,62.9 c -1.3,2.6 -5.4,4 -9.2,3 z"
+           inkscape:connector-curvature="0"
+           style="fill:#54af39" />
+        <path
+           id="path_1"
+           d="m 229.662,292.5 v 0 c 3.7,-0.9 5.7,-3.8 4.3,-6.5 l -32.3,-62.9 c -1.3,-2.6 -5.5,-4 -9.2,-3 v 0 c -3.7,0.9 -5.7,3.8 -4.3,6.5 l 32.3,62.9 c 1.3,2.6 5.5,4 9.2,3 z"
+           inkscape:connector-curvature="0"
+           style="fill:#54af39" />
+        <path
+           id="path_2"
+           d="m 151.362,36.9 -0.6,0.2 c -1.8,0.7 -3.8,-0.3 -4.5,-2.1 l -9.6,-26.7 c -0.7,-1.8 0.3,-3.8 2.1,-4.5 l 0.6,-0.2 c 1.8,-0.7 3.8,0.3 4.5,2.1 l 9.6,26.7 c 0.7,1.9 -0.3,3.9 -2.1,4.5 z"
+           inkscape:connector-curvature="0"
+           style="fill:#54af39" />
+        <path
+           id="path_3"
+           d="m 180.662,3.8 v 0 c 0,2.1 -1.7,3.8 -3.8,3.8 h -61 c -2.1,0 -3.8,-1.7 -3.8,-3.8 v 0 c 0,-2.1 1.7,-3.8 3.8,-3.8 h 61 c 2.1,0 3.8,1.7 3.8,3.8 z"
+           inkscape:connector-curvature="0"
+           style="fill:#54af39" />
+        <path
+           id="path_4"
+           d="m 218.762,236.7 h -144.9 c -13.3,0 -24,-10.8 -24,-24 v -108 c 0,-41.3 33.5,-74.9 74.9,-74.9 h 43.3 c 41.3,0 74.9,33.5 74.9,74.9 v 108 c -0.1,13.2 -10.9,24 -24.2,24 z"
+           inkscape:connector-curvature="0"
+           style="fill:#54af39" />
+        <path
+           id="path_5"
+           d="m 212.562,146.2 h -132.5 c -5.6,0 -10.2,-4.5 -10.2,-10.2 v -34.9 c 0,-16.9 13.7,-30.6 30.6,-30.6 h 91.7 c 16.9,0 30.6,13.7 30.6,30.6 V 136 c -0.1,5.7 -4.6,10.2 -10.2,10.2 z"
+           inkscape:connector-curvature="0"
+           style="fill:#ffffff" />
+        <path
+           id="path_6"
+           d="m 161.462,55.5 h -30.3 c -3.2,0 -5.7,-2.6 -5.7,-5.7 v 0 c 0,-3.2 2.6,-5.7 5.7,-5.7 h 30.3 c 3.2,0 5.7,2.6 5.7,5.7 v 0 c 0,3.1 -2.6,5.7 -5.7,5.7 z"
+           inkscape:connector-curvature="0"
+           style="fill:#ffffff" />
+        <path
+           id="path_7"
+           d="m 87.062,191.9 m -14.8,0 c 0,-3.924 1.56,-7.691 4.335,-10.465 2.774,-2.775 6.541,-4.335 10.465,-4.335 3.924,0 7.691,1.56 10.465,4.335 2.775,2.774 4.335,6.541 4.335,10.465 0,3.924 -1.56,7.691 -4.335,10.465 -2.774,2.775 -6.541,4.335 -10.465,4.335 -3.924,0 -7.691,-1.56 -10.465,-4.335 -2.775,-2.774 -4.335,-6.541 -4.335,-10.465"
+           inkscape:connector-curvature="0"
+           style="fill:#ffffff" />
+        <path
+           id="path_8"
+           d="m 205.662,191.9 m -14.8,0 c 0,-3.924 1.56,-7.691 4.335,-10.465 2.774,-2.775 6.541,-4.335 10.465,-4.335 3.924,0 7.691,1.56 10.465,4.335 2.775,2.774 4.335,6.541 4.335,10.465 0,3.924 -1.56,7.691 -4.335,10.465 -2.774,2.775 -6.541,4.335 -10.465,4.335 -3.924,0 -7.691,-1.56 -10.465,-4.335 -2.775,-2.774 -4.335,-6.541 -4.335,-10.465"
+           inkscape:connector-curvature="0"
+           style="fill:#ffffff" />
+      </g>
     </g>
   </g>
   <g




diff --git a/build.gradle.kts b/build.gradle.kts
index 46c4cc081eaca93cfd488ae20a55802bb1a9de63..7a3bc819960b47c4f4e7c762aa8cc8e828d14f11 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,8 +4,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later
 
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 plugins {
-    id("com.android.application") version "8.5.1" apply false
-    id("com.android.library") version "8.5.1" apply false
+    id("com.android.application") version "8.7.0" apply false
+    id("com.android.library") version "8.7.0" apply false
     kotlin("android") version "2.0.10" apply false
     kotlin("jvm") version "1.7.20" apply false
     kotlin("plugin.parcelize") version "1.8.20" apply false




diff --git a/fruchtfleisch/build.gradle.kts b/fruchtfleisch/build.gradle.kts
index d8428614c918943ac054616ce6052d6d5a72fc33..014557871091925f8b0568606aea1e74ec7837d9 100644
--- a/fruchtfleisch/build.gradle.kts
+++ b/fruchtfleisch/build.gradle.kts
@@ -8,8 +8,8 @@     kotlin("jvm")
 }
 
 dependencies {
-    testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
-    testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
+    testImplementation("org.junit.jupiter:junit-jupiter:5.11.2")
+    testImplementation("org.junit.jupiter:junit-jupiter:5.11.2")
 
     //implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.10")
 }
@@ -21,4 +21,4 @@ }
 
 tasks.withType<Test> {
     useJUnitPlatform()
-}
\ No newline at end of file
+}




diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e6b146979591f464343c7a948c2493b91598885d..dfec7cc51514aaa2b56c69a2e03179fbb0370502 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -4,7 +4,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later
 
 #Tue Aug 09 15:48:25 CEST 2022
 distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
 distributionPath=wrapper/dists
 zipStorePath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME




diff --git a/metadata/en/changelogs/32.txt b/metadata/en/changelogs/32.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f4add26464b804086eccfaebbaf8250a4b6d8452
--- /dev/null
+++ b/metadata/en/changelogs/32.txt
@@ -0,0 +1,2 @@
+* Added basic Transitous support
+* Fixed phantom localities names in search results




diff --git a/metadata/en-US/changelogs/32.txt b/metadata/en-US/changelogs/32.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f4add26464b804086eccfaebbaf8250a4b6d8452
--- /dev/null
+++ b/metadata/en-US/changelogs/32.txt
@@ -0,0 +1,2 @@
+* Added basic Transitous support
+* Fixed phantom localities names in search results




diff --git a/metadata/pl-PL/changelogs/32.txt b/metadata/pl-PL/changelogs/32.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4eaba3d3347c664e684ced97d5c921f52eda0670
--- /dev/null
+++ b/metadata/pl-PL/changelogs/32.txt
@@ -0,0 +1,2 @@
+* Dodano podstawową obsługę Transitous
+* Naprawiono fantomowe nazwy lokalizacji w wynikach wyszukiwania




diff --git a/release.sh b/release.sh
index 62122ff378c7bd472d9e463b2b145ef1637b4669..528c6853c80625ca544bd4addf3b535280ea079b 100755
--- a/release.sh
+++ b/release.sh
@@ -62,13 +62,21 @@ 	latestCIstarted=$(echo "$latestCI" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+\+[0-9]{2}:[0-9]{2}' | head -n1)
 	while [ "$latestCIstatus" != 'OK' ] && [ "$retry" = "1" ]
 	do
 		echo "latest CI started at $latestCIstarted result is $latestCIstatus, not OK"
-		echo "retry? [y/N]"
+		echo "retry? [y/N/s]"
 		read -r decision
-		if [ "$decision" != 'y' ]
+		if [ "$decision" = 'N' ]
 		then
 			retry="0"
 			exit 1
 		fi
+		if [ "$decision" = 's' ]
+		then
+			retry="0"
+			break
+		fi
+		latestCI=$(curl https://ci.apiote.xyz/toys/czwek-commitly/latest 2>/dev/null)
+		latestCIstatus=$(echo "$latestCI" | grep '<h2' | sed 's/<h2[^>]*>//' | sed 's|</h2>||' | grep -oE '[A-Z]+')
+		latestCIstarted=$(echo "$latestCI" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+\+[0-9]{2}:[0-9]{2}' | head -n1)
 	done
 
 	currentVersionName=$(grep -Eo 'versionName = "[0-9\.]+"' app/build.gradle.kts | cut -d '=' -f2 | tr -d ' "')