Bimba.git

commit 24013ca674ccd60e94aeca16c421caa9ea205086

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

use elizabeth dev responses to show inexact times and lines in change options

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


diff --git a/app/src/debug/java/xyz/apiote/bimba/czwek/api/responses/DevResponses.kt b/app/src/debug/java/xyz/apiote/bimba/czwek/api/responses/DevResponses.kt
index e7e36dc7168480786df2c912ee01962d0bfb3826..bb77730d3df8a610a21d21b72dbb39116f45d245 100644
--- a/app/src/debug/java/xyz/apiote/bimba/czwek/api/responses/DevResponses.kt
+++ b/app/src/debug/java/xyz/apiote/bimba/czwek/api/responses/DevResponses.kt
@@ -5,11 +5,11 @@
 package xyz.apiote.bimba.czwek.api.responses
 
 import xyz.apiote.bimba.czwek.api.AlertV1
-import xyz.apiote.bimba.czwek.api.DepartureV4
+import xyz.apiote.bimba.czwek.api.DepartureV5
 import xyz.apiote.bimba.czwek.api.LineV3
-import xyz.apiote.bimba.czwek.api.LocatableV3
-import xyz.apiote.bimba.czwek.api.QueryableV4
-import xyz.apiote.bimba.czwek.api.StopV2
+import xyz.apiote.bimba.czwek.api.LocatableV4
+import xyz.apiote.bimba.czwek.api.QueryableV5
+import xyz.apiote.bimba.czwek.api.StopV3
 import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException
 import xyz.apiote.bimba.czwek.api.VehicleV3
 import xyz.apiote.bimba.czwek.api.structs.FeedInfoV2
@@ -18,13 +18,13 @@ import java.io.InputStream
 
 data class DeparturesResponseDev(
 	val alerts: List<AlertV1>,
-	val departures: List<DepartureV4>,
-	val stop: StopV2
+	val departures: List<DepartureV5>,
+	val stop: StopV3
 ) : DeparturesResponse {
 	companion object {
 		fun unmarshal(stream: InputStream): DeparturesResponseDev {
 			val alerts = mutableListOf<AlertV1>()
-			val departures = mutableListOf<DepartureV4>()
+			val departures = mutableListOf<DepartureV5>()
 
 			val reader = Reader(stream)
 			val alertsNum = reader.readUInt().toULong()
@@ -34,11 +34,11 @@ 				alerts.add(alert)
 			}
 			val departuresNum = reader.readUInt().toULong()
 			for (i in 0UL until departuresNum) {
-				val departure = DepartureV4.unmarshal(stream)
+				val departure = DepartureV5.unmarshal(stream)
 				departures.add(departure)
 			}
 
-			return DeparturesResponseDev(alerts, departures, StopV2.unmarshal(stream))
+			return DeparturesResponseDev(alerts, departures, StopV3.unmarshal(stream))
 		}
 	}
 }
@@ -69,16 +69,16 @@ 		}
 	}
 }
 
-data class LocatablesResponseDev(val locatables: List<LocatableV3>) : LocatablesResponse {
+data class LocatablesResponseDev(val locatables: List<LocatableV4>) : LocatablesResponse {
 	companion object {
 		fun unmarshal(stream: InputStream): LocatablesResponseDev {
-			val locatables = mutableListOf<LocatableV3>()
+			val locatables = mutableListOf<LocatableV4>()
 			val reader = Reader(stream)
 			val n = reader.readUInt().toULong()
 			for (i in 0UL until n) {
 				when (val r = reader.readUInt().toULong()) {
 					0UL -> {
-						locatables.add(StopV2.unmarshal(stream))
+						locatables.add(StopV3.unmarshal(stream))
 					}
 
 					1UL -> {
@@ -96,16 +96,16 @@ 	}
 }
 
 
-data class QueryablesResponseDev(val queryables: List<QueryableV4>) : QueryablesResponse {
+data class QueryablesResponseDev(val queryables: List<QueryableV5>) : QueryablesResponse {
 	companion object {
 		fun unmarshal(stream: InputStream): QueryablesResponseDev {
-			val queryables = mutableListOf<QueryableV4>()
+			val queryables = mutableListOf<QueryableV5>()
 			val reader = Reader(stream)
 			val n = reader.readUInt().toULong()
 			for (i in 0UL until n) {
 				when (val r = reader.readUInt().toULong()) {
 					0UL -> {
-						queryables.add(StopV2.unmarshal(stream))
+						queryables.add(StopV3.unmarshal(stream))
 					}
 
 					1UL -> {




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 76953700a4a76e06a41716bcdcc43d07e50e1008..0d66dc74963b07142e773eb533bda2804a66e12a 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
@@ -82,7 +82,11 @@ 		"queryables",
 		null,
 		params,
 		context,
-		arrayOf(1u, 2u, 3u, 4u),
+		if (server.apiPath == "https://bimba.apiote.xyz/next") {
+			arrayOf(1u, 2u, 3u, 4u, 0u)
+		} else {
+			arrayOf(1u, 2u, 3u, 4u)
+		},
 		null
 	)
 }




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Interfaces.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Interfaces.kt
index 4bfc2f5c505cd365ec5a973bc01a4558856f35c9..ae3a82af89e9bd12e3796f093a0ad2d14362c6a0 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Interfaces.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Interfaces.kt
@@ -8,6 +8,8 @@ interface QueryableV1
 interface QueryableV2
 interface QueryableV3
 interface QueryableV4
+interface QueryableV5
 interface LocatableV1
 interface LocatableV2
-interface LocatableV3
\ No newline at end of file
+interface LocatableV3
+interface LocatableV4
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt
index 9e604c62e01918732d5d03565aa5492cb5989dd5..62d9c8a13101ed601d9234c19ba9b2b8551b6f9b 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt
@@ -198,7 +198,8 @@ 		}
 	}
 }
 
-data class ColourV1(val R: UByte, val G: UByte, val B: UByte) {
+@Parcelize
+data class ColourV1(val R: UByte, val G: UByte, val B: UByte): Parcelable {
 	companion object {
 		fun unmarshal(stream: InputStream): ColourV1 {
 			val reader = Reader(stream)
@@ -309,7 +310,7 @@ 	val Line: LineStubV3,
 	val Headsign: String,
 	val CongestionLevel: CongestionLevelV1,
 	val OccupancyStatus: OccupancyStatusV1
-) : LocatableV3 {
+) : LocatableV3, LocatableV4 {
 	companion object {
 		fun unmarshal(stream: InputStream): VehicleV3 {
 			val reader = Reader(stream)
@@ -327,9 +328,10 @@ 		}
 	}
 }
 
+@Parcelize
 data class LineStubV1(
 	val name: String, val kind: LineTypeV1, val colour: ColourV1
-) {
+):Parcelable {
 	companion object {
 		fun unmarshal(stream: InputStream): LineStubV1 {
 			val reader = Reader(stream)
@@ -470,6 +472,76 @@ 		}
 	}
 }
 
+data class DepartureV5(
+	val ID: String,
+	val time: Time,
+	val status: VehicleStatusV1,
+	val isRealtime: Boolean,
+	val vehicle: VehicleV3,
+	val boarding: UByte,
+	val alerts: List<AlertV1>,
+	val exact: Boolean,
+	val terminusArrival: Boolean
+) {
+
+	companion object {
+		fun unmarshal(stream: InputStream): DepartureV5 {
+			val reader = Reader(stream)
+			val id = reader.readString()
+			val time = Time.unmarshal(stream)
+			val status = VehicleStatusV1.of(reader.readUInt().toULong().toUInt())
+			val isRealtime = reader.readBoolean()
+			val vehicle = VehicleV3.unmarshal(stream)
+			val boarding = reader.readU8()
+			val alertsNum = reader.readUInt().toULong()
+			val alerts = mutableListOf<AlertV1>()
+			for (i in 0UL until alertsNum) {
+				alerts.add(AlertV1.unmarshal(stream))
+			}
+			val exact = reader.readBoolean()
+			val terminusArrival = reader.readBoolean()
+			return DepartureV5(id, time, status, isRealtime, vehicle, boarding, alerts, exact, terminusArrival)
+		}
+	}
+}
+
+@Parcelize
+data class StopV3(
+	val code: String,
+	val name: String,
+	val nodeName: String,
+	val zone: String,
+	val feedID: String,
+	val position: PositionV1,
+	val changeOptions: List<ChangeOptionV2>
+) : Parcelable, LocatableV4, QueryableV5 {
+	companion object {
+		fun unmarshal(stream: InputStream): StopV3 {
+			val reader = Reader(stream)
+			val code = reader.readString()
+			val name = reader.readString()
+			val nodeName = reader.readString()
+			val zone = reader.readString()
+			val feedID = reader.readString()
+			val position = PositionV1.unmarshal(stream)
+			val chOptionsNum = reader.readUInt().toULong()
+			val changeOptions = mutableListOf<ChangeOptionV2>()
+			for (i in 0UL until chOptionsNum) {
+				changeOptions.add(ChangeOptionV2.unmarshal(stream))
+			}
+			return StopV3(
+				name = name,
+				nodeName = nodeName,
+				code = code,
+				zone = zone,
+				position = position,
+				feedID = feedID,
+				changeOptions = changeOptions
+			)
+		}
+	}
+}
+
 @Parcelize
 data class StopV2(
 	val code: String,
@@ -632,7 +704,7 @@ 	val type: LineTypeV3,
 	val feedID: String,
 	val headsigns: List<List<String>>,
 	val graphs: List<LineGraph>,
-) : QueryableV4 {
+) : QueryableV4, QueryableV5 {
 	override fun toString(): String {
 		return "$name ($type) [$colour]\n${headsigns.map { "-> ${it.joinToString()}" }}"
 	}
@@ -722,6 +794,21 @@ 				9u -> valueOf("FUNICULAR")
 				10u -> valueOf("MONORAIL")
 				else -> throw UnknownResourceVersionException("LineType/$type", 3u)
 			}
+		}
+	}
+}
+
+@Parcelize
+data class ChangeOptionV2(val line: LineStubV1, val headsigns: List<String>) : Parcelable {
+	companion object {
+		fun unmarshal(stream: InputStream): ChangeOptionV2 {
+			val reader = Reader(stream)
+			val line = LineStubV1.unmarshal(stream)
+			val headsignsNum = reader.readUInt().toULong().toInt()
+			val headsigns = (0 until headsignsNum).map {
+				reader.readString()
+			}
+			return ChangeOptionV2(line = line, headsigns = headsigns)
 		}
 	}
 }




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Departures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Departures.kt
index 0d053162b88f3d4f2a65975d1d2cff92e2b979a4..4e22e00b1b8365b32fc365f86c904dd74366651c 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Departures.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Departures.kt
@@ -19,7 +19,7 @@ 	companion object {
 		fun unmarshal(stream: InputStream): DeparturesResponse {
 			val reader = Reader(stream)
 			return when (val v = reader.readUInt().toULong()) {
-				// 0UL -> DeparturesResponseDev.unmarshal(stream)
+				0UL -> DeparturesResponseDev.unmarshal(stream)
 				1UL -> DeparturesResponseV1.unmarshal(stream)
 				2UL -> DeparturesResponseV2.unmarshal(stream)
 				3UL -> DeparturesResponseV3.unmarshal(stream)




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Locatables.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Locatables.kt
index a176bd2dff780abb44aa27b89af2679233679fe7..2188e29c3297dc5614ce30f40b4bb359a67781f0 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Locatables.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Locatables.kt
@@ -21,7 +21,7 @@ 	companion object {
 		fun unmarshal(stream: InputStream): LocatablesResponse {
 			val reader = Reader(stream)
 			return when (val v = reader.readUInt().toULong()) {
-				// 0UL -> LocatablesResponseDev.unmarshal(stream)
+				0UL -> LocatablesResponseDev.unmarshal(stream)
 				1UL -> LocatablesResponseV1.unmarshal(stream)
 				2UL -> LocatablesResponseV2.unmarshal(stream)
 				3UL -> LocatablesResponseV3.unmarshal(stream)
@@ -34,7 +34,7 @@
 
 data class LocatablesResponseV3(val locatables: List<LocatableV3>) : LocatablesResponse {
 	companion object {
-		fun unmarshal(stream: InputStream): LocatablesResponseDev {
+		fun unmarshal(stream: InputStream): LocatablesResponseV3 {
 			val locatables = mutableListOf<LocatableV3>()
 			val reader = Reader(stream)
 			val n = reader.readUInt().toULong()
@@ -53,7 +53,7 @@ 						throw UnknownResourceVersionException("Locatable/$r", 0u)
 					}
 				}
 			}
-			return LocatablesResponseDev(locatables)
+			return LocatablesResponseV3(locatables)
 		}
 	}
 }




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Queryables.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Queryables.kt
index 8e7cc8f8c7a63204b4c2d69604ae33f2fe4ac768..216ab34e80475121dde939626338c14e59e382cb 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Queryables.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/responses/Queryables.kt
@@ -22,7 +22,7 @@ 	companion object {
 		fun unmarshal(stream: InputStream): QueryablesResponse {
 			val reader = Reader(stream)
 			return when (val v = reader.readUInt().toULong()) {
-				// 0UL -> QueryablesResponseDev.unmarshal(stream)
+				0UL -> QueryablesResponseDev.unmarshal(stream)
 				1UL -> QueryablesResponseV1.unmarshal(stream)
 				2UL -> QueryablesResponseV2.unmarshal(stream)
 				3UL -> QueryablesResponseV3.unmarshal(stream)




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 054385b6785da668fc461fee763808be6f371166..6761cf6abca84e34c17a37b63e36ca2fe1992b9e 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
@@ -187,7 +187,7 @@ 					Toast.makeText(context, ctx.getString(R.string.no_map_app), Toast.LENGTH_SHORT).show()
 				}
 			}
 
-			stop.changeOptions(ctx).let { changeOptions ->
+			stop.changeOptions(ctx, Stop.LineDecoration.NONE).let { changeOptions ->
 				content.findViewById<TextView>(R.id.change_options).apply {
 					text = changeOptions.first
 					contentDescription = changeOptions.second




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 bd20eaf7e474954a96ea993491dddebf413e1fb9..a59744516071cdef554282c14ffd0a342a9b3fff 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
@@ -51,6 +51,7 @@ 	val lineIcon: ImageView = itemView.findViewById(R.id.line_icon)
 	val departureTime: TextView = itemView.findViewById(R.id.departure_time)
 	val lineName: TextView = itemView.findViewById(R.id.departure_line)
 	val headsign: TextView = itemView.findViewById(R.id.departure_headsign)
+	val timeStatus: ImageView = itemView.findViewById(R.id.time_status)
 
 	companion object {
 		fun bind(
@@ -73,8 +74,31 @@ 				context?.getString(
 					R.string.departure_headsign_content_description,
 					departure.vehicle.Headsign
 				)
+			when {
+				departure.isRealtime -> {
+					holder?.timeStatus?.setImageResource(R.drawable.radar)
+					holder?.timeStatus?.contentDescription = context?.getString(R.string.realtime_content_description)
+					holder?.timeStatus?.let { TooltipCompat.setTooltipText(it, context?.getString(R.string.realtime_content_description)) }
+				}
+				departure.exact -> {
+					holder?.timeStatus?.setImageResource(R.drawable.calendar)
+					holder?.timeStatus?.contentDescription = context?.getString(R.string.exact_content_description)
+					holder?.timeStatus?.let { TooltipCompat.setTooltipText(it, context?.getString(R.string.exact_content_description)) }
+				}
+				else -> {
+					holder?.timeStatus?.setImageResource(R.drawable.inexact)
+					holder?.timeStatus?.contentDescription = context?.getString(R.string.inexact_content_description)
+					holder?.timeStatus?.let { TooltipCompat.setTooltipText(it, context?.getString(R.string.inexact_content_description)) }
+				}
+			}
 
 			holder?.departureTime?.text = departure.statusText(context, showAsTime)
+			if (departure.terminusArrival) {
+				holder?.departureTime?.alpha = .5f
+				holder?.lineIcon?.alpha = .5f
+				holder?.lineName?.alpha = .5f
+				holder?.headsign?.alpha = .5f
+			}
 		}
 	}
 }
@@ -138,7 +162,9 @@ 		override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
 			val oldDeparture = oldDepartures[oldItemPosition]
 			val newDeparture = newDepartures[newItemPosition]
 			return if (oldDeparture.departure != null && newDeparture.departure != null) {
-				oldDeparture.departure.vehicle.Line == newDeparture.departure.vehicle.Line &&
+				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,




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
index 1164836fb36f512ff578e26a75e9c1ac04013cac..7cb5e9c07c7093ca8c6fada65b4d80e54bff7e30 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
@@ -447,7 +447,9 @@ 		stop: Stop?,
 		leaveAlert: Boolean = false
 	) {
 		setupSnackbar()
-		binding.departuresRecycler.scrollToPosition(0)
+		if (adapter.itemCount == 0) {
+			binding.departuresRecycler.scrollToPosition(0)
+		}
 		binding.departuresProgress.visibility = View.GONE
 		// TODO [elizabeth] max, progress from header Cache-Control max-age
 		binding.departuresUpdatesProgress.apply {




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/ChangeOption.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/ChangeOption.kt
index b41f089279301f16959982baf3d256b52c5512ad..110e85d8f3bea00613dcb3c89ed024dac1a4f4f4 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/ChangeOption.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/ChangeOption.kt
@@ -5,7 +5,9 @@
 package xyz.apiote.bimba.czwek.repo
 
 import xyz.apiote.bimba.czwek.api.ChangeOptionV1
+import xyz.apiote.bimba.czwek.api.ChangeOptionV2
 
-data class ChangeOption(val line: String, val headsign: String) {
-	constructor(c: ChangeOptionV1) : this(c.line, c.headsign)
+data class ChangeOption(val line: LineStub, val headsigns: List<String>) {
+	constructor(c: ChangeOptionV1) : this(LineStub(c.line, LineType.UNKNOWN, Colour(0u,0u,0u)), listOf(c.headsign))
+	constructor(c: ChangeOptionV2) : this(LineStub(c.line), c.headsigns)
 }
\ 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 b3459fda1568096322caab7e16e7dace0d9b87a6..66ac62094ec48793ce330634aa369da2d1b18c68 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
@@ -14,6 +14,7 @@ import xyz.apiote.bimba.czwek.api.DepartureV1
 import xyz.apiote.bimba.czwek.api.DepartureV2
 import xyz.apiote.bimba.czwek.api.DepartureV3
 import xyz.apiote.bimba.czwek.api.DepartureV4
+import xyz.apiote.bimba.czwek.api.DepartureV5
 import xyz.apiote.bimba.czwek.api.Time
 import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException
 import xyz.apiote.bimba.czwek.units.Second
@@ -115,7 +116,9 @@ 	val status: ULong,
 	val isRealtime: Boolean,
 	val vehicle: Vehicle,
 	val boarding: UByte,
-	val alerts: List<Alert>
+	val alerts: List<Alert>,
+	val exact: Boolean,
+	val terminusArrival: Boolean
 ) {
 
 	constructor(d: DepartureV1) : this(
@@ -125,7 +128,9 @@ 		d.status,
 		d.isRealtime,
 		Vehicle(d.vehicle),
 		d.boarding,
-		emptyList()
+		emptyList(),
+		true,
+		false
 	)
 
 	constructor(d: DepartureV2) : this(
@@ -135,7 +140,9 @@ 		d.status,
 		d.isRealtime,
 		Vehicle(d.vehicle),
 		d.boarding,
-		emptyList()
+		emptyList(),
+		true,
+		false
 	)
 
 	constructor(d: DepartureV3) : this(
@@ -145,7 +152,9 @@ 		d.status.ordinal.toULong(), // TODO VehicleStatus
 		d.isRealtime,
 		Vehicle(d.vehicle),
 		d.boarding,
-		emptyList()
+		emptyList(),
+		true,
+		false
 	)
 
 	constructor(d: DepartureV4) : this(
@@ -155,7 +164,21 @@ 		d.status.ordinal.toULong(), // TODO VehicleStatus
 		d.isRealtime,
 		Vehicle(d.vehicle),
 		d.boarding,
-		d.alerts.map { Alert(it) }
+		d.alerts.map { Alert(it) },
+		true,
+		false
+	)
+
+	constructor(d: DepartureV5) : this(
+		d.ID,
+		d.time,
+		d.status.ordinal.toULong(), // TODO VehicleStatus
+		d.isRealtime,
+		Vehicle(d.vehicle),
+		d.boarding,
+		d.alerts.map { Alert(it) },
+		d.exact,
+		d.terminusArrival
 	)
 
 	fun statusText(context: Context?, showAsTime: Boolean, at: ZonedDateTime? = null): String {
@@ -195,12 +218,13 @@ 		}
 	}
 
 	fun timeString(context: Context): String {
-		return if (isRealtime) {
-			context.getString(
+		return when {
+			isRealtime -> context.getString(
 				R.string.at_time_realtime, time.Hour.toInt(), time.Minute.toInt(), time.Second.toInt()
 			)
-		} else {
-			context.getString(R.string.at_time, time.Hour.toInt(), time.Minute.toInt())
+
+			exact -> context.getString(R.string.at_time, time.Hour.toInt(), time.Minute.toInt())
+			else -> context.getString(R.string.about_time, time.Hour.toInt(), time.Minute.toInt())
 		}
 	}
 




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt
index 3d645e655861a1f67f3694aa12da1ab90eb1d675..d7d1924cfa5e28c9a41b7433bde9aa7608279cdf 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt
@@ -18,7 +18,7 @@ import java.util.Locale
 
 class FeedInfoPrev {
 	companion object {
-		fun unmarshal(stream: InputStream): FeedInfo {
+		fun unmarshal(@Suppress("UNUSED_PARAMETER") stream: InputStream): FeedInfo {
 			return FeedInfo(FeedInfoPrev())
 		}
 	}
@@ -100,7 +100,7 @@ 		null,
 		cached
 	)
 
-	constructor(f: FeedInfoPrev) : this(
+	constructor(@Suppress("UNUSED_PARAMETER") f: FeedInfoPrev) : this(
 		"",
 		"",
 		"",




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 d5ecc9bbbba7111c55e31d0015ce15f696e17274..db2525fa84c91a88fb53b7d6c1b40de93540f8d7 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
@@ -67,9 +67,9 @@ 			LineType.TROLLEYBUS -> R.drawable.trolleybus_black
 			LineType.METRO -> R.drawable.metro_black
 			LineType.RAIL -> R.drawable.train_black
 			LineType.FERRY -> R.drawable.ferry_black
-			LineType.CABLE_TRAM -> TODO()
-			LineType.CABLE_CAR -> TODO()
-			LineType.FUNICULAR -> TODO()
+			LineType.CABLE_TRAM -> R.drawable.cablecar_black
+			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
 		}




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 1c3f36595fe1c6bc646a137d9475294d59f0f9d1..1ba24e3cbe16562fe30e5fb186d1087c948659c4 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
@@ -14,6 +14,7 @@ import xyz.apiote.bimba.czwek.api.PositionV1
 import xyz.apiote.bimba.czwek.api.Server
 import xyz.apiote.bimba.czwek.api.StopV1
 import xyz.apiote.bimba.czwek.api.StopV2
+import xyz.apiote.bimba.czwek.api.StopV3
 import xyz.apiote.bimba.czwek.api.UnknownResourceException
 import xyz.apiote.bimba.czwek.api.VehicleV1
 import xyz.apiote.bimba.czwek.api.VehicleV2
@@ -171,7 +172,7 @@ 			return when (val response =
 				withContext(Dispatchers.IO) { LocatablesResponse.unmarshal(result.stream!!) }) {
 				is LocatablesResponseDev -> response.locatables.map {
 					when (it) {
-						is StopV2 -> Stop(it)
+						is StopV3 -> Stop(it)
 						is VehicleV3 -> Vehicle(it)
 						else -> throw UnknownResourceException("locatables", it::class)
 					}
@@ -273,7 +274,7 @@ 			return when (val response =
 				withContext(Dispatchers.IO) { QueryablesResponse.unmarshal(result.stream!!) }) {
 				is QueryablesResponseDev -> response.queryables.map {
 					when (it) {
-						is StopV2 -> Stop(it)
+						is StopV3 -> Stop(it)
 						is LineV3 -> Line(it)
 						else -> throw UnknownResourceException("queryablesV4", it::class)
 					}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt
index 0c6909006f92fe6a5bc7add619c14d4b7cc7a0a4..662b6d26c696a2096f634ecfab49e7af6d7aacdb 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt
@@ -5,10 +5,20 @@
 package xyz.apiote.bimba.czwek.repo
 
 import android.content.Context
+import android.graphics.Typeface
 import android.graphics.drawable.Drawable
+import android.text.Annotation
+import android.text.Spannable
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.SpannedString
+import android.text.style.BackgroundColorSpan
+import android.text.style.StyleSpan
+import androidx.preference.PreferenceManager
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.api.StopV1
 import xyz.apiote.bimba.czwek.api.StopV2
+import xyz.apiote.bimba.czwek.api.StopV3
 
 data class Stop(
 	val code: String,
@@ -46,22 +56,118 @@ 		s.feedID,
 		Position(s.position),
 		s.changeOptions.map { ChangeOption(it) })
 
-	fun changeOptions(context: Context): Pair<String, String> = Pair(changeOptions.groupBy { it.line }
-		.map { Pair(it.key, it.value.joinToString { co -> co.headsign }) }.joinToString {
-			context.getString(
-				R.string.vehicle_headsign, it.first, it.second
-			)
-		},
-		changeOptions.groupBy { it.line }
-			.map { Pair(it.key, it.value.joinToString { co -> co.headsign }) }.joinToString {
-				context.getString(
-					R.string.vehicle_headsign_content_description, it.first, it.second
+	constructor(s: StopV3) : this(
+		s.code,
+		s.name,
+		s.nodeName,
+		s.zone,
+		s.feedID,
+		Position(s.position),
+		s.changeOptions.map { ChangeOption(it) })
+
+	fun changeOptions(context: Context, decoration: LineDecoration): Pair<Spannable, String> {
+		return Pair(changeOptions.groupBy { it.line }
+			.map {
+				Pair(
+					it.key,
+					it.value.flatMap { co -> co.headsigns }.sortedBy { headsign -> headsign }.joinToString()
+				)
+			}.fold(SpannableStringBuilder("")) { acc, p ->
+				if (acc.toString() != "") {
+					acc.append("; ")
+				}
+				var str = SpannableStringBuilder(
+					context.getText(
+						R.string.vehicle_headsign
+					) as SpannedString
 				)
-			})
+				str = applyAnnotations(str, decoration, p.first, p.first.name, p.second)
+				str = applyAnnotations(str, decoration, p.first)
+				acc.append(str)
+				acc
+			},
+			changeOptions.groupBy { it.line }
+				.map {
+					Pair(
+						it.key,
+						it.value.flatMap { co -> co.headsigns }.sortedBy { headsign -> headsign }.joinToString()
+					)
+				}.joinToString {
+					context.getString(
+						R.string.vehicle_headsign_content_description, it.first, it.second
+					)
+				})
+	}
+
+	private fun applyAnnotations(
+		s: SpannableStringBuilder,
+		decoration: LineDecoration,
+		line: LineStub,
+		vararg args: Any
+	): SpannableStringBuilder {
+		val str = SpannableStringBuilder(s)
+		val annotations = str.getSpans(0, str.length, Annotation::class.java)
+		annotations.forEach {
+			when (it.key) {
+				"arg" -> {
+					if (args.isEmpty()) {
+						return@forEach
+					}
+					val argIndex = Integer.parseInt(it.value)
+					str.replace(str.getSpanStart(it), str.getSpanEnd(it), args[argIndex] as String)
+				}
+
+				"decoration" -> {
+					if (args.isNotEmpty()) {
+						return@forEach
+					}
+					val background = BackgroundColorSpan(line.colour.toInt())
+					val foreground = line.textColour(line.colour)
+					val ital = StyleSpan(Typeface.ITALIC)
+					when (decoration) {
+						LineDecoration.ITALICS -> str.setSpan(
+							ital,
+							str.getSpanStart(it),
+							str.getSpanEnd(it),
+							Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+						)
+
+						LineDecoration.COLOUR -> {
+							str.setSpan(background, str.getSpanStart(it), str.getSpanEnd(it), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+							str.setSpan(foreground, str.getSpanStart(it), str.getSpanEnd(it), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+						}
+
+						LineDecoration.NONE -> {}
+					}
+				}
+			}
+		}
+		return str
+	}
+
+	fun changeOptionsString(): String = changeOptions.groupBy { it.line }
+		.map {
+			Pair(
+				it.key,
+				it.value.flatMap { co -> co.headsigns }.sortedBy { headsign -> headsign }.joinToString()
+			)
+		}.joinToString("; ")
 
 	override fun toString(): String {
-		var result = "$name ($code) [$zone] $position\n"
-		for (chOpt in changeOptions) result += "${chOpt.line} → ${chOpt.headsign}\n"
-		return result
+		return "$name ($code) [$zone] $position\n${changeOptionsString()}"
+	}
+
+	enum class LineDecoration {
+		NONE, ITALICS, COLOUR;
+		companion object {
+			fun fromPreferences(context: Context) =
+				when (PreferenceManager.getDefaultSharedPreferences(context)
+					.getString("line_decoration", "italics")) {
+					"italics" -> Stop.LineDecoration.ITALICS
+					"colour" -> Stop.LineDecoration.COLOUR
+					"none" -> Stop.LineDecoration.NONE
+					else -> Stop.LineDecoration.ITALICS
+				}
+		}
 	}
 }
\ 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 7ce6dc062432c68e176a752e609cc2b966dbef52..a13611c7ee852e3421c01a0bdc651128d0ef2d6f 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
@@ -144,7 +144,7 @@ 				holder?.feedName?.visibility = View.VISIBLE
 				holder?.feedName?.text = feeds?.get(stop.feedID)?.name ?: ""
 			}
 			context?.let {
-				stop.changeOptions(it).let { changeOptions ->
+				stop.changeOptions(it, Stop.LineDecoration.fromPreferences(context)).let { changeOptions ->
 					holder?.description?.apply {
 						text = changeOptions.first
 						contentDescription = changeOptions.second
@@ -257,10 +257,8 @@ 				}
 
 				is Stop -> {
 					assert(newQueryable is Stop)
-					val oldChangeOptions =
-						oldQueryable.changeOptions.joinToString { "${it.line}->${it.headsign}" }
-					val newChangeOptions =
-						(newQueryable as Stop).changeOptions.joinToString { "${it.line}->${it.headsign}" }
+					val oldChangeOptions = oldQueryable.changeOptionsString()
+					val newChangeOptions = (newQueryable as Stop).changeOptionsString()
 					oldQueryable.name == newQueryable.name && oldChangeOptions == newChangeOptions &&
 						oldPosition?.latitude == newPosition?.latitude &&
 						oldPosition?.longitude == newPosition?.longitude &&




diff --git a/app/src/main/res/drawable/cablecar_black.xml b/app/src/main/res/drawable/cablecar_black.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4f2979fcb7103f73915f871cf103dcba9e06c3b9
--- /dev/null
+++ b/app/src/main/res/drawable/cablecar_black.xml
@@ -0,0 +1,17 @@
+<!--
+SPDX-FileCopyrightText: Austin Andrews
+
+SPDX-License-Identifier: Apache-2.0
+
+source: https://pictogrammers.com/library/mdi/icon/gondola/
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+	<path
+		android:fillColor="#000000"
+		android:pathData="M18,10H13V7.59L22.12,6.07L21.88,4.59L16.41,5.5C16.46,5.35 16.5,5.18 16.5,5A1.5,1.5 0 0,0 15,3.5A1.5,1.5 0 0,0 13.5,5C13.5,5.35 13.63,5.68 13.84,5.93L13,6.07V5H11V6.41L10.41,6.5C10.46,6.35 10.5,6.18 10.5,6A1.5,1.5 0 0,0 9,4.5A1.5,1.5 0 0,0 7.5,6C7.5,6.36 7.63,6.68 7.83,6.93L1.88,7.93L2.12,9.41L11,7.93V10H6C4.89,10 4,10.9 4,12V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18V12A2,2 0 0,0 18,10M6,12H8.25V16H6V12M9.75,16V12H14.25V16H9.75M18,16H15.75V12H18V16Z" />
+</vector>
\ No newline at end of file




diff --git a/app/src/main/res/drawable/cabletram_black.xml b/app/src/main/res/drawable/cabletram_black.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3947ee16f0d93ca3e4c67b9e861a0309e6ec255b
--- /dev/null
+++ b/app/src/main/res/drawable/cabletram_black.xml
@@ -0,0 +1,44 @@
+<!--
+SPDX-FileCopyrightText: Jamison Wieser
+
+SPDX-License-Identifier: CC0-1.0
+
+source: https://thenounproject.com/icon/cable-car-4173/
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:viewportWidth="1600"
+	android:viewportHeight="1600">
+	<group
+		android:translateX="200"
+		android:translateY="200">
+		<path
+			android:fillColor="#000000"
+			android:pathData="m166.5,228v48.2h48.2v413.8h770.7v-413.9h48.1v-48.2c-289,-96.3 -578,-96.3 -867,0zM455.5,641.8h-192.6v-316.5c64.2,-38.5 128.4,-38.5 192.6,0 -0,38.5 -0,316.5 -0,316.5zM696.3,641.8h-192.6v-316.5c64.2,-38.5 128.4,-38.5 192.6,0 -0,38.5 -0,316.5 -0,316.5zM937.1,641.8h-192.6v-316.5c64.2,-38.5 128.4,-38.5 192.6,0v316.5z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m214.7,714.6v262.6h770.7l-0,-262.6zM600,894.8c-26.6,0 -48.2,-21.6 -48.2,-48.2 0,-26.6 21.6,-48.1 48.2,-48.1 26.6,0 48.2,21.5 48.2,48.1 0,26.6 -21.6,48.2 -48.2,48.2z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m985.3,1055.5v-48.2h-770.7v48.2h-96.3v48.1h125.2c10.6,0 25.4,-6.1 32.9,-13.6l21,-21c6.2,-6.2 17.3,-11.4 27,-13 16.7,2.7 19.2,15 4.6,29.6l-84.2,84.2c-16.6,16.6 -11,30.2 12.4,30.2h685.4c23.4,0 29.1,-13.6 12.5,-30.2l-84.2,-84.2c-14.5,-14.5 -12,-26.8 4.7,-29.6 9.6,1.7 20.8,6.8 27,13l20.9,21c7.5,7.5 22.3,13.6 32.9,13.6h125.2v-48.1h-96.3z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m359.2,146.4c192.6,-24.1 289,-24.1 481.6,0v-24.1h48.2v-24.1c-192.7,-48.2 -385.3,-48.2 -578,0v24.1h48.2v24.1z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m648.2,46c-1.2,-25.6 -22.3,-46 -48.2,-46 -25.9,0 -47,20.4 -48.2,46 32.1,-2.8 64.3,-2.8 96.4,0z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m189.4,446c0,-10.8 -8.8,-19.6 -19.7,-19.6s-19.6,8.8 -19.6,19.6v458.3c0,10.8 8.7,19.6 19.6,19.6 10.9,0 19.7,-8.8 19.7,-19.6z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m1049.9,446c0,-10.8 -8.8,-19.6 -19.6,-19.6 -10.9,0 -19.7,8.8 -19.7,19.6v458.3c0,10.8 8.8,19.6 19.7,19.6 10.8,0 19.6,-8.8 19.6,-19.6z" />
+	</group>
+</vector>




diff --git a/app/src/main/res/drawable/funicular_black.xml b/app/src/main/res/drawable/funicular_black.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3472477cc7d19902ba293b28b182f8ab6a52d069
--- /dev/null
+++ b/app/src/main/res/drawable/funicular_black.xml
@@ -0,0 +1,25 @@
+<!--
+SPDX-FileCopyrightText: Daniel Calliess
+
+SPDX-License-Identifier: CC0-1.0
+
+source: https://thenounproject.com/icon/funicular-635171/
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:viewportWidth="1600"
+	android:viewportHeight="1600">
+	<group
+		android:translateX="200"
+		android:translateY="200">
+		<path
+			android:fillColor="#000000"
+			android:pathData="m60,1086 l1080,-624.6v54l-1080,624.6z" />
+
+		<path
+			android:fillColor="#000000"
+			android:pathData="m81.6,663c-12,6.9 -21.6,22.1 -21.6,34.1v313.3c0,12 9.6,16 21.6,9.1l1036.8,-599.6c12,-6.9 21.6,-22.1 21.6,-34.1v-313.3c0,-12 -9.6,-16 -21.6,-9.1zM135.6,696.6 L387.7,550.8c6,-3.5 10.8,-1.4 10.8,4.6v172.9c0,6 -4.8,13.6 -10.8,17.1l-252.1,145.8c-6,3.5 -10.8,1.4 -10.8,-4.6v-172.9c0,-6 4.8,-13.6 10.8,-17.1zM474.1,500.8 L725.9,355.2c6,-3.5 10.8,-1.4 10.8,4.6v172.9c0,6 -4.8,13.6 -10.8,17.1l-251.8,145.6c-6,3.5 -10.8,1.4 -10.8,-4.6v-172.9c0,-6 4.8,-13.6 10.8,-17.1zM812.3,305.2 L1064.4,159.4c6,-3.5 10.8,-1.4 10.8,4.6v172.9c0,6 -4.8,13.6 -10.8,17.1l-252.1,145.8c-6,3.5 -10.8,1.4 -10.8,-4.6v-172.9c0,-6 4.8,-13.6 10.8,-17.1z" />
+	</group>
+</vector>




diff --git a/app/src/main/res/drawable/inexact.xml b/app/src/main/res/drawable/inexact.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d1ba0403097cbc7a980bc9d5ddfd921596069d04
--- /dev/null
+++ b/app/src/main/res/drawable/inexact.xml
@@ -0,0 +1,17 @@
+<!--
+SPDX-FileCopyrightText: Michael Irigoyen
+
+SPDX-License-Identifier: Apache-2.0
+
+source: https://pictogrammers.com/library/mdi/icon/tilde/
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+	<path
+		android:fillColor="#000000"
+		android:pathData="M2,15C2,15 2,9 8,9C12,9 12.5,12.5 15.5,12.5C19.5,12.5 19.5,9 19.5,9H22C22,9 22,15 16,15C12,15 10.5,11.5 8.5,11.5C4.5,11.5 4.5,15 4.5,15H2" />
+</vector>
\ No newline at end of file




diff --git a/app/src/main/res/drawable/info.xml b/app/src/main/res/drawable/info.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2131db7742b29fd42abe5a1bf96f90e2233eadae
--- /dev/null
+++ b/app/src/main/res/drawable/info.xml
@@ -0,0 +1,18 @@
+<!--
+SPDX-FileCopyrightText: Google
+
+SPDX-License-Identifier: Apache-2.0
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
+
+</vector>




diff --git a/app/src/main/res/drawable/settings.xml b/app/src/main/res/drawable/settings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e363b3e6e6567aa5db2c040f4432ddd803247b10
--- /dev/null
+++ b/app/src/main/res/drawable/settings.xml
@@ -0,0 +1,18 @@
+<!--
+SPDX-FileCopyrightText: Google
+
+SPDX-License-Identifier: Apache-2.0
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" />
+
+</vector>




diff --git a/app/src/main/res/drawable/speed.xml b/app/src/main/res/drawable/speed.xml
index 67e8c0ca8f57a67bc7480bc1d8ba887928814491..139f3480d252286fa5ef0522b2d89b0f5300426c 100644
--- a/app/src/main/res/drawable/speed.xml
+++ b/app/src/main/res/drawable/speed.xml
@@ -4,8 +4,13 @@
 SPDX-License-Identifier: Apache-2.0
 -->
 
-<vector android:height="24dp" android:tint="?attr/colorOnSurface"
-    android:viewportHeight="24" android:viewportWidth="24"
-    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
-    <path android:fillColor="@android:color/white" android:pathData="M20.38,8.57l-1.23,1.85a8,8 0,0 1,-0.22 7.58L5.07,18A8,8 0,0 1,15.58 6.85l1.85,-1.23A10,10 0,0 0,3.35 19a2,2 0,0 0,1.72 1h13.85a2,2 0,0 0,1.74 -1,10 10,0 0,0 -0.27,-10.44zM10.59,15.41a2,2 0,0 0,2.83 0l5.66,-8.49 -8.49,5.66a2,2 0,0 0,0 2.83z"/>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="?attr/colorOnSurface"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M20.38,8.57l-1.23,1.85a8,8 0,0 1,-0.22 7.58L5.07,18A8,8 0,0 1,15.58 6.85l1.85,-1.23A10,10 0,0 0,3.35 19a2,2 0,0 0,1.72 1h13.85a2,2 0,0 0,1.74 -1,10 10,0 0,0 -0.27,-10.44zM10.59,15.41a2,2 0,0 0,2.83 0l5.66,-8.49 -8.49,5.66a2,2 0,0 0,0 2.83z" />
 </vector>




diff --git a/app/src/main/res/drawable/vehicle.xml b/app/src/main/res/drawable/vehicle.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6f99d217d5e9cf41d2ea910980fd338244a1b1f0
--- /dev/null
+++ b/app/src/main/res/drawable/vehicle.xml
@@ -0,0 +1,11 @@
+<!--
+SPDX-FileCopyrightText: Google
+
+SPDX-License-Identifier: Apache-2.0
+-->
+
+<vector android:height="24dp" android:tint="?attr/colorOnSurface"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M12,4L5,4C3.34,4 2,5.34 2,7v8c0,1.66 1.34,3 3,3l-1,1v1h1l2,-2.03L9,18v-5L4,13L4,5.98L13,6v2h2L15,7c0,-1.66 -1.34,-3 -3,-3zM5,14c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM20.57,9.66c-0.14,-0.4 -0.52,-0.66 -0.97,-0.66h-7.19c-0.46,0 -0.83,0.26 -0.98,0.66L10,13.77l0.01,5.51c0,0.38 0.31,0.72 0.69,0.72h0.62c0.38,0 0.68,-0.38 0.68,-0.76L12,18h8v1.24c0,0.38 0.31,0.76 0.69,0.76h0.61c0.38,0 0.69,-0.34 0.69,-0.72l0.01,-1.37v-4.14l-1.43,-4.11zM12.41,10h7.19l1.03,3h-9.25l1.03,-3zM12,16c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM20,16c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
+</vector>




diff --git a/app/src/main/res/layout/departure.xml b/app/src/main/res/layout/departure.xml
index 834cbe0773dbb70fe4aa6a9c8f26ac4d5b114224..b2514262349aef0d448fed775a6116b4042ece6d 100644
--- a/app/src/main/res/layout/departure.xml
+++ b/app/src/main/res/layout/departure.xml
@@ -37,14 +37,16 @@ 		tool:text="1hr" />
 
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/departure_line"
-		android:layout_width="wrap_content"
+		android:layout_width="0dp"
 		android:layout_height="wrap_content"
 		android:layout_marginStart="8dp"
 		android:layout_marginTop="8dp"
+		android:layout_marginEnd="8dp"
 		android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
 		app:layout_constraintStart_toEndOf="@+id/line_icon"
 		app:layout_constraintTop_toTopOf="parent"
-		tool:text="Metropolitan" />
+		app:layout_constraintEnd_toStartOf="@id/departure_time"
+		tool:text="Circle" />
 
 	<com.google.android.material.textview.MaterialTextView
 		android:id="@+id/departure_headsign"
@@ -55,5 +57,13 @@ 		app:layout_constraintStart_toStartOf="@+id/departure_line"
 		app:layout_constraintTop_toBottomOf="@+id/departure_line"
 		tool:text="» Tower Hill" />
 
-
+	<ImageView
+		android:id="@+id/time_status"
+		android:layout_width="12dp"
+		android:layout_height="12dp"
+		android:layout_marginEnd="8dp"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/departure_time"
+		tool:ignore="ContentDescription"
+		tool:srcCompat="@drawable/inexact" />
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file




diff --git a/app/src/main/res/menu/drawer.xml b/app/src/main/res/menu/drawer.xml
index 1d876c0cf374716191449dcfaf664152687c4623..5a5194c75d78419d3249ea66b2dc7a1e1354d202 100644
--- a/app/src/main/res/menu/drawer.xml
+++ b/app/src/main/res/menu/drawer.xml
@@ -25,9 +25,11 @@ 		
 	</item>
 	<!-- other settings -->
 	<item
+		android:icon="@drawable/settings"
 	android:id="@+id/drawer_settings"
 	android:title="@string/title_settings"/>
 	<item
+		android:icon="@drawable/info"
 		android:id="@+id/drawer_about"
 		android:title="@string/title_about"/>
 </menu>
\ No newline at end of file




diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 3b5e70a97a94517eebe0bf7f8d1b7273fd45f931..7cdbd4ec2b08693dbea5f88f9c457baea7dae35e 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -46,4 +46,16 @@ 		@string/neen
 		<item>@string/dex</item>
 		<item>@string/lef</item>
 	</string-array>
+
+	<string-array name="line_decorations">
+		<item>@string/italics</item>
+		<item>@string/colour</item>
+		<item>@string/none</item>
+	</string-array>
+
+	<string-array name="line_decorations_values">
+		<item>italics</item>
+		<item>colour</item>
+		<item>none</item>
+	</string-array>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8e5ca43fe47402d84cefdf881ac378ccd664757d..5544a08fa8e80201a2b7dc37609f9afdfc7921d9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -29,7 +29,7 @@ 	You are offline. Connect to the Internet
 	<string name="error_gps">Cannot obtain current location</string>
 	<string name="no_departures">No departures</string>
 	<string name="waiting_position">waiting for position</string>
-	<string name="vehicle_headsign">%1$s » %2$s</string>
+	<string name="vehicle_headsign"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
 	<string name="distance_in_m">%1$s m</string>
 	<plurals name="distance_in_m_cd">
 		<item quantity="one">%1$d metre</item>
@@ -131,6 +131,7 @@ 	momentarily
 	<string name="departure_departed">departed</string>
 	<string name="departure_now">now</string>
 	<string name="at_time">at %1$02d:%2$02d</string>
+	<string name="about_time">about %1$02d:%2$02d</string>
 	<string name="at_time_realtime">at %1$02d:%2$02d:%3$02d</string>
 	<string name="on_demand">on demand</string>
 	<string name="no_boarding">no boarding</string>
@@ -146,6 +147,10 @@ 	Results for ‘%1$s’
 	<string name="bimba_server_address_hint">Server</string>
 	<string name="bimba_server_token_hint">Token</string>
 	<string name="realtime_content_description">departure is realtime</string>
+	<!-- cf timepoint field in https://gtfs.org/schedule/reference/#stop_timestxt -->
+	<string name="exact_content_description">departure time is exact from schedule</string>
+	<!-- cf timepoint field in https://gtfs.org/schedule/reference/#stop_timestxt -->
+	<string name="inexact_content_description">departure time is approximate from schedule</string>
 	<string name="wheelchair_content_description">vehicle is wheelchair accessible</string>
 	<string name="air_condition_content_description">air conditioning</string>
 	<string name="bicycles_allowed_content_description">bicycles allowed</string>
@@ -259,4 +264,8 @@ 	neen
 	<string name="dex">dex</string>
 	<string name="lef">lef</string>
 	<string name="filtered_stop_question">Do you want to save a favourite filtered with selected lines?</string>
+	<string name="none">none</string>
+	<string name="italics">italics</string>
+	<string name="colour">colour</string>
+	<string name="line_decorations">Line name decorations</string>
 </resources>




diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 0cd1a0dacf54562ef83c5f01f304f305c18a6ec6..23ea65341109cbdcd89d71536547c76ec2212e18 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -9,7 +9,7 @@     Karte
     <string name="home_fab_description">GPS-Symbol</string>
     <string name="title_activity_results">Ergebnisse</string>
     <string name="continue_">Fortsetzen</string>
-    <string name="vehicle_headsign">%1$s » %2$s</string>
+    <string name="vehicle_headsign"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
     <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="congestion_unknown">unbekannt</string>
     <string name="congestion_congestion">Stau</string>




diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml
index 8afbdd1697a0c2c92063444971b7bb220891b8e1..2deff940846040ba2348cc2cd33de82f6664e315 100644
--- a/app/src/main/res/values-en-rUS/strings.xml
+++ b/app/src/main/res/values-en-rUS/strings.xml
@@ -26,7 +26,7 @@ 	You are offline. Connect to the Internet
     <string name="error_gps">Cannot obtain current location</string>
     <string name="no_departures">No departures</string>
     <string name="waiting_position">waiting for position</string>
-    <string name="vehicle_headsign">%1$s » %2$s</string>
+    <string name="vehicle_headsign"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
     <string name="distance_in_m">%1$s m</string>
     <plurals name="distance_in_m_cd">
         <item quantity="one">%1$d meter</item>




diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 681fc303bc0dd98da77ad08144d901815fb93f97..1a44d63c4dfbb2e513512abbbe81adabb33a2ef4 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -16,7 +16,7 @@     Il y a eu une erreur sur le serveur. Réessayez plus tard
     <string name="error_connecting">Erreur lors de la connection au serveur. Réessayez plus tard</string>
     <string name="error_unknown">Erreur inconnue</string>
     <string name="waiting_position">En attente de la position</string>
-    <string name="vehicle_headsign">%1$s » %2$s</string>
+    <string name="vehicle_headsign"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
     <string name="congestion_unknown">Inconnu</string>
     <string name="congestion_smooth">Fluide</string>
     <string name="occupancy_standing_only">Uniquement debout</string>




diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 69ecbb6d8a8a72b7e4f4af3003ecdcb28aebaf0a..9b4c97506c9d76ed958272f6fa3adac7d6357eb4 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -25,7 +25,7 @@     Sei offline. Collega al’Internet
     <string name="error_gps">Non è possibile ottenere la posizione corrente</string>
     <string name="no_departures">Nessune partenze</string>
     <string name="waiting_position">In attesa della posizione</string>
-    <string name="vehicle_headsign">%1$s » %2$s</string>
+    <string name="vehicle_headsign"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
     <string name="vehicle_headsign_content_description">%1$s verso %2$s</string>
     <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="congestion_unknown">sconosciuta</string>




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index c91cb8c9b197de575f215cf9b77488998cebbdc7..4725a9872ad279a61f8db367a6fa5641a30414e8 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -25,7 +25,7 @@     Brak połączenia z Internetem
     <string name="error_gps">Nie można uzyskać bierzącej pozycji</string>
     <string name="no_departures">Brak odjazdów</string>
     <string name="waiting_position">oczekiwanie na pozycję</string>
-    <string name="vehicle_headsign">%1$s » %2$s</string>
+    <string name="vehicle_headsign"><annotation decoration="apply"><annotation arg="0">%1$s</annotation></annotation> » <annotation arg="1">%2$s</annotation></string>
     <string name="vehicle_headsign_content_description">%1$s w kierunku przystanku %2$s</string>
     <string name="speed_in_km_per_h">%1$s km/h</string>
     <string name="congestion_unknown">nieznane</string>




diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index ab4959ff16b28aba10d9e4f2e244473c76f04fe2..4d40eb2a6f5b696bc91d93df8492454c72f5f80b 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -21,4 +21,16 @@ 			app:icon="@drawable/download"
 			app:key="download_cities_list"
 			app:title="Update cities list now" />
 	</PreferenceCategory>
+
+	<PreferenceCategory app:title="Appearance">
+		<ListPreference
+			app:defaultValue="italics"
+			app:entries="@array/line_decorations"
+			app:entryValues="@array/line_decorations_values"
+			app:icon="@drawable/vehicle"
+			app:key="line_decoration"
+			app:title="@string/line_decorations"
+			app:useSimpleSummaryProvider="true"
+			/>
+	</PreferenceCategory>
 </PreferenceScreen>
\ No newline at end of file