Bimba.git

commit 12e673926463d9144faf16696cc945a21f8c762d

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

separate arrivals and departures in events list

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


diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt
index 66fa6ae249f756a186b7405f5794b29726efd1db..3c52446a49d0db59620b89fa7ac64a29bd774cf5 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt
@@ -12,16 +12,15 @@ 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.Event
 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.StopEvents
 import xyz.apiote.bimba.czwek.repo.TrafficResponseException
 import xyz.apiote.bimba.czwek.repo.Vehicle
 import xyz.apiote.bimba.czwek.units.Mps
@@ -37,7 +36,7 @@ 	context: Context,
 	stop: String,
 	date: LocalDate?,
 	limit: Int?
-): StopDepartures {
+): StopEvents {
 	if (!isNetworkAvailable(context)) {
 		throw TrafficResponseException(0, "", Error(0, R.string.error_offline, R.drawable.error_net))
 	}
@@ -75,7 +74,7 @@ 			throw TrafficResponseException(result.error.statusCode, "", result.error)
 		}
 	} else {
 		return withContext(Dispatchers.IO) {
-			val departures = mutableListOf<Departure>()
+			val events = mutableListOf<Event>()
 			var stopID = ""
 			var stopName = ""
 			var latitude = 0.0
@@ -190,10 +189,17 @@ 										ZonedDateTime.ofInstant(
 											Instant.ofEpochSecond(eventTimestamp),
 											ZoneId.systemDefault()
 										)
-									departures.add(
-										Departure(
-											ID = hash,
-											time = Time(
+									events.add(
+										Event(
+											id = hash,
+											arrivalTime = Time(
+												t.hour.toUInt(),
+												t.minute.toUInt(),
+												t.second.toUInt(),
+												(t.dayOfYear - ZonedDateTime.now().dayOfYear).toByte(),
+												ZoneId.systemDefault().id
+											),
+											departureTime = Time(
 												t.hour.toUInt(),
 												t.minute.toUInt(),
 												t.second.toUInt(),
@@ -247,8 +253,8 @@ 					}
 				}
 			}
 
-			return@withContext StopDepartures(
-				departures,
+			return@withContext StopEvents(
+				events,
 				Stop(stopID, stopName, stopName, "", "transitous", Position(latitude, longitude), listOf()),
 				listOf()
 			)




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt
index 2b1d50a476c8181b6758489d60b61fe772080d09..bc7ebbe9a139b516dc13113707601dffa5f0826b 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/Favourites.kt
@@ -14,7 +14,7 @@ import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.RecyclerView
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.departures.DeparturesActivity
-import xyz.apiote.bimba.czwek.repo.Departure
+import xyz.apiote.bimba.czwek.repo.Event
 import xyz.apiote.bimba.czwek.repo.Favourite
 import java.time.ZoneId
 import java.time.ZonedDateTime
@@ -23,7 +23,7 @@ import java.util.Optional
 
 class BimbaFavouritesAdapter(
 	private var favourites: List<Favourite>,
-	private var departures: Map<String, Optional<Departure>>,
+	private var departures: Map<String, Optional<Event>>,
 	private val inflater: LayoutInflater,
 	private val context: Context
 ) :
@@ -34,9 +34,9 @@ 		private set
 
 	inner class DiffUtilCallback(
 		private val oldFavourites: List<Favourite>,
-		private val oldDepartures: Map<String, Optional<Departure>?>,
+		private val oldDepartures: Map<String, Optional<Event>?>,
 		private val newFavourites: List<Favourite>,
-		private val newDepartures: Map<String, Optional<Departure>?>
+		private val newDepartures: Map<String, Optional<Event>?>
 	) : DiffUtil.Callback() {
 		override fun getOldListSize() = oldFavourites.size
 
@@ -73,7 +73,7 @@ 			if ((oldDeparture!!.isEmpty && !newDeparture!!.isEmpty) || (!oldDeparture!!.isEmpty && newDeparture!!.isEmpty)) {
 				return false
 			}
 
-			return oldDeparture!!.get().ID == newDeparture!!.get().ID &&
+			return oldDeparture!!.get().id == newDeparture!!.get().id &&
 				oldDeparture!!.get().vehicle.Line == newDeparture!!.get().vehicle.Line &&
 				oldDeparture!!.get().vehicle.Headsign == newDeparture!!.get().vehicle.Headsign &&
 				oldDeparture!!.get().statusText(
@@ -111,7 +111,7 @@ 		this.favourites = favourites
 		diff.dispatchUpdatesTo(this)
 	}
 
-	fun updateDepartures(departures: Map<String, Optional<Departure>>) {
+	fun updateDepartures(departures: Map<String, Optional<Event>>) {
 		this.departures = departures
 		notifyDataSetChanged()
 		lastUpdate = ZonedDateTime.now()
@@ -151,8 +151,8 @@ class FavouriteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
 	val root: View = itemView.findViewById(R.id.favourite)
 	val feedName: TextView = itemView.findViewById(R.id.feed_name)
 	val lineIcon: ImageView = itemView.findViewById(R.id.line_icon)
-	val departureTime: TextView = itemView.findViewById(R.id.departure_time)
-	val departureTimeFull: TextView = itemView.findViewById(R.id.departure_full_time)
+	val arrivalTime: TextView = itemView.findViewById(R.id.arrival_time)
+	val arrivalTimeFull: TextView = itemView.findViewById(R.id.departure_full_time)
 	val lineName: TextView = itemView.findViewById(R.id.departure_line)
 	val headsign: TextView = itemView.findViewById(R.id.departure_headsign)
 	val stopHeadline: TextView = itemView.findViewById(R.id.stop_name)
@@ -162,33 +162,34 @@ 		fun bind(
 			favourite: Favourite,
 			holder: FavouriteViewHolder,
 			context: Context,
-			departure: Optional<Departure>?
+			event: Optional<Event>?
 		) {
-			if (departure == null) {
+			if (event == null) {
 				holder.feedName.text = favourite.feedName
 				holder.stopHeadline.text = favourite.stopName
 				holder.lineIcon.setImageDrawable(null)
 				holder.lineName.text = context.getString(R.string.loading)
-				holder.departureTime.text = ""
-				holder.departureTimeFull.text = ""
+				holder.arrivalTime.text = ""
+				holder.arrivalTimeFull.text = ""
 				holder.headsign.text = ""
-			} else if (departure.isEmpty) {
+			} else if (event.isEmpty) {
 				holder.feedName.text = favourite.feedName
 				holder.stopHeadline.text = favourite.stopName
 				holder.lineIcon.setImageDrawable(null)
 				holder.lineName.text = context.getString(R.string.no_departures).lowercase()
-				holder.departureTime.text = ""
-				holder.departureTimeFull.text = ""
+				holder.arrivalTime.text = ""
+				holder.arrivalTimeFull.text = ""
 				holder.headsign.text = ""
 			} else {
-				val vehicle = departure.get().vehicle
+				val statusTexts =event.get().statusText(context, false)
+				val vehicle = event.get().vehicle
 				holder.feedName.text = favourite.feedName
 				holder.stopHeadline.text = favourite.stopName
 				holder.lineIcon.setImageDrawable(vehicle.Line.icon(context))
 				holder.lineIcon.contentDescription = vehicle.Line.kind.name
 				holder.lineName.text = vehicle.Line.name
-				holder.departureTime.text = departure.get().statusText(context, false)
-				holder.departureTimeFull.text = departure.get().timeString(context)
+				holder.arrivalTime.text = statusTexts.first
+				holder.arrivalTimeFull.text = event.get().arrivalTimeString(context)
 				holder.headsign.text =
 					context.getString(R.string.departure_headsign, vehicle.Headsign)
 				holder.headsign.contentDescription =




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt
index ba6da66f9308e7dc91bd81aca46cff37c3dffee1..def65cf9bf6e3e4d1f37c37e65d5121123a5824d 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt
@@ -18,7 +18,7 @@ import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
-import xyz.apiote.bimba.czwek.repo.Departure
+import xyz.apiote.bimba.czwek.repo.Event
 import xyz.apiote.bimba.czwek.repo.Favourite
 import xyz.apiote.bimba.czwek.repo.FeedInfo
 import xyz.apiote.bimba.czwek.repo.OfflineRepository
@@ -36,8 +36,8 @@ 	var feeds: Map? = null
 	var feedsSettings: FeedsSettings? = null
 	private val mutableFavourites = MutableLiveData<List<Favourite>>()
 	val favourites: LiveData<List<Favourite>> = mutableFavourites
-	private val mutableDepartures = MutableLiveData<Map<String, Optional<Departure>>>()
-	val departures: LiveData<Map<String, Optional<Departure>>> = mutableDepartures
+	private val mutableDepartures = MutableLiveData<Map<String, Optional<Event>>>()
+	val departures: LiveData<Map<String, Optional<Event>>> = mutableDepartures
 
 	fun getQueryables(query: String, context: Context) {
 		viewModelScope.launch {
@@ -89,10 +89,10 @@ 								context,
 								12  // XXX heuristics
 							)
 							stopDepartures?.let { sDs ->
-								if (sDs.departures.isEmpty()) {
+								if (sDs.events.isEmpty()) {
 									Pair(favourite.feedID+favourite.stopCode, Optional.empty())
 								} else {
-									Pair(favourite.feedID+favourite.stopCode, Optional.ofNullable(sDs.departures.find { departure ->
+									Pair(favourite.feedID+favourite.stopCode, Optional.ofNullable(sDs.events.find { departure ->
 											favourite.lines.isEmpty() or favourite.lines.contains(
 												departure.vehicle.Line.name
 											)




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 1f732ae3e9c31ed6ecc8dd9c8a343738ea0ff0ac..c730072452d546a0c5e2491786ed586a65e0c694 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
@@ -38,8 +38,8 @@ import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.dpToPixelI
 import xyz.apiote.bimba.czwek.repo.Alert
 import xyz.apiote.bimba.czwek.repo.CongestionLevel
-import xyz.apiote.bimba.czwek.repo.Departure
-import xyz.apiote.bimba.czwek.repo.DepartureItem
+import xyz.apiote.bimba.czwek.repo.Event
+import xyz.apiote.bimba.czwek.repo.EventItem
 import xyz.apiote.bimba.czwek.repo.OccupancyStatus
 import xyz.apiote.bimba.czwek.repo.Vehicle
 import xyz.apiote.bimba.czwek.units.UnitSystem
@@ -49,37 +49,40 @@
 class BimbaDepartureViewHolder(itemView: View) : ViewHolder(itemView) {
 	val root: View = itemView.findViewById(R.id.departure)
 	val lineIcon: ImageView = itemView.findViewById(R.id.line_icon)
+	val arrivalTime: TextView = itemView.findViewById(R.id.arrival_time)
+	val arrivalStatus: TextView = itemView.findViewById(R.id.arrival_status)
 	val departureTime: TextView = itemView.findViewById(R.id.departure_time)
+	val departureStatus: TextView = itemView.findViewById(R.id.departure_status)
 	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)
+	val eventStatus: ImageView = itemView.findViewById(R.id.event_status)
 
 	companion object {
 		fun bind(
-			departure: Departure,
+			event: Event,
 			holder: BimbaDepartureViewHolder?,
 			context: Context?,
 			showAsTime: Boolean,
-			onClickListener: (Departure) -> Unit,
+			onClickListener: (Event) -> Unit,
 			showingTerminusArrivals: String
 		) {
 			holder?.root?.setOnClickListener {
-				onClickListener(departure)
+				onClickListener(event)
 			}
-			holder?.lineIcon?.setImageDrawable(departure.vehicle.Line.icon(context!!))
-			holder?.lineIcon?.contentDescription = departure.vehicle.Line.kind.name
-			holder?.lineName?.text = departure.vehicle.Line.name
+			holder?.lineIcon?.setImageDrawable(event.vehicle.Line.icon(context!!))
+			holder?.lineIcon?.contentDescription = event.vehicle.Line.kind.name
+			holder?.lineName?.text = event.vehicle.Line.name
 			holder?.headsign?.text =
-				context?.getString(R.string.departure_headsign, departure.vehicle.Headsign)
+				context?.getString(R.string.departure_headsign, event.vehicle.Headsign)
 			holder?.headsign?.contentDescription =
 				context?.getString(
 					R.string.departure_headsign_content_description,
-					departure.vehicle.Headsign
+					event.vehicle.Headsign
 				)
 
 			when {
-				departure.isRealtime -> {
-					holder?.timeStatus?.let {
+				event.isRealtime -> {
+					holder?.eventStatus?.let {
 						it.contentDescription =
 							context?.getString(R.string.realtime_content_description)
 						it.setImageResource(R.drawable.radar)
@@ -98,36 +101,52 @@ 							.start()*/
 					}
 				}
 
-				departure.exact -> {
+				event.exact -> {
 					// FIXME clear animation
-					holder?.timeStatus?.let {
+					holder?.eventStatus?.let {
 						it.setImageResource(R.drawable.calendar)
 						it.contentDescription =
-							context?.getString(R.string.exact_content_description)
+							context?.getString(R.string.schedule_content_description)
 						TooltipCompat.setTooltipText(
 							it,
-							context?.getString(R.string.exact_content_description)
+							context?.getString(R.string.schedule_content_description)
 						)
 					}
 				}
+			}
 
-				else -> {
-					// FIXME clear animation
-					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)
-						)
+			val statusTexts = event.statusText(context, showAsTime)
+			if (event.arrivalTime == event.departureTime) {
+				holder?.arrivalStatus?.visibility = View.GONE
+				holder?.arrivalTime?.text = statusTexts.first
+				holder?.departureStatus?.visibility = View.GONE
+				holder?.departureTime?.visibility = View.GONE
+			} else {
+				if (statusTexts.first != null) {
+					holder?.arrivalTime?.text = statusTexts.first
+					holder?.arrivalStatus?.text = if (!event.exact) {
+						context?.getString(R.string.arrival_approximate)
+					} else {
+						context?.getString(R.string.arrival)
 					}
+				} else {
+					holder?.arrivalTime?.visibility = View.GONE
+					holder?.arrivalStatus?.visibility = View.GONE
+				}
+				if (statusTexts.second != null) {
+					holder?.departureTime?.text = statusTexts.second
+					holder?.departureStatus?.text = if (!event.exact) {
+						context?.getString(R.string.departure_approximate)
+					} else {
+						context?.getString(R.string.departure)
+					}
+				} else {
+					holder?.departureTime?.visibility = View.GONE
+					holder?.departureStatus?.visibility = View.GONE
 				}
 			}
-
-			holder?.departureTime?.text = departure.statusText(context, showAsTime)
 			holder?.root?.alpha =
-				if (departure.terminusArrival && showingTerminusArrivals == BimbaDeparturesAdapter.TERMINUS_ARRIVAL_GREY_OUT) {
+				if (event.terminusArrival && showingTerminusArrivals == BimbaDeparturesAdapter.TERMINUS_ARRIVAL_GREY_OUT) {
 					.5f
 				} else {
 					1f
@@ -169,8 +188,8 @@
 class BimbaDeparturesAdapter(
 	private val inflater: LayoutInflater,
 	private val context: Context?,
-	private var items: List<DepartureItem>,
-	private val onClickListener: ((Departure) -> Unit),
+	private var items: List<EventItem>,
+	private val onClickListener: ((Event) -> Unit),
 ) :
 	RecyclerView.Adapter<ViewHolder>() {
 
@@ -197,8 +216,8 @@ 		private set
 	private var showAsTime: Boolean = false
 
 	inner class DiffUtilCallback(
-		private val oldDepartures: List<DepartureItem>,
-		private val newDepartures: List<DepartureItem>,
+		private val oldDepartures: List<EventItem>,
+		private val newDepartures: List<EventItem>,
 		private val showAsTimeChanged: Boolean,
 	) : DiffUtil.Callback() {
 		override fun getOldListSize() = oldDepartures.size
@@ -206,23 +225,23 @@
 		override fun getNewListSize() = newDepartures.size
 
 		override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
-			(oldDepartures[oldItemPosition].departure?.ID
-				?: ALERT_ITEM_ID) == (newDepartures[newItemPosition].departure?.ID ?: ALERT_ITEM_ID)
+			(oldDepartures[oldItemPosition].event?.id
+				?: ALERT_ITEM_ID) == (newDepartures[newItemPosition].event?.id ?: ALERT_ITEM_ID)
 
 		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.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
+			return if (oldDeparture.event != null && newDeparture.event != null) {
+				!oldDeparture.event.terminusArrival &&
+						oldDeparture.event.terminusArrival == newDeparture.event.terminusArrival &&
+						oldDeparture.event.exact == newDeparture.event.exact &&
+						oldDeparture.event.vehicle.Line == newDeparture.event.vehicle.Line &&
+						oldDeparture.event.vehicle.Headsign == newDeparture.event.vehicle.Headsign &&
+						oldDeparture.event.statusText(
+							context,
+							false,
+							lastUpdate
+						) == newDeparture.event.statusText(context, false) && !showAsTimeChanged
 			} else if (oldDeparture.alert.isNotEmpty() && newDeparture.alert.isEmpty()) {
 				oldDeparture.alert == newDeparture.alert
 			} else {
@@ -235,12 +254,12 @@ 	private var departuresPositions: MutableMap = HashMap()
 
 	init {
 		items.forEachIndexed { i, departure ->
-			departuresPositions[departure.departure?.ID ?: ALERT_ITEM_ID] = i
+			departuresPositions[departure.event?.id ?: ALERT_ITEM_ID] = i
 		}
 	}
 
 	override fun getItemViewType(position: Int): Int {
-		return if (items[position].departure != null) {
+		return if (items[position].event != null) {
 			0
 		} else {
 			1
@@ -260,7 +279,7 @@
 	override fun onBindViewHolder(holder: ViewHolder, position: Int) {
 		if (holder is BimbaDepartureViewHolder) {
 			BimbaDepartureViewHolder.bind(
-				items[position].departure!!,
+				items[position].event!!,
 				holder,
 				context,
 				showAsTime,
@@ -274,7 +293,7 @@ 	}
 
 	override fun getItemCount(): Int = items.size
 
-	fun get(id: String): DepartureItem? {
+	fun get(id: String): EventItem? {
 		val position = departuresPositions[id]
 		return if (position == null) {
 			null
@@ -284,7 +303,7 @@ 		}
 	}
 
 	fun update(
-		departures: List<DepartureItem>,
+		departures: List<EventItem>,
 		showAsTime: Boolean,
 		areNewObserved: Boolean = false,
 		leaveAlert: Boolean = false
@@ -296,7 +315,7 @@ 			departures
 		}
 		val newPositions: MutableMap<String, Int> = HashMap()
 		newDepartures.forEachIndexed { i, departure ->
-			newPositions[departure.departure?.ID ?: ALERT_ITEM_ID] = i
+			newPositions[departure.event?.id ?: ALERT_ITEM_ID] = i
 		}
 		val diff = DiffUtil.calculateDiff(
 			DiffUtilCallback(
@@ -321,7 +340,7 @@ 		update(this.items, showAsTime)
 	}
 }
 
-class DepartureBottomSheet(private var departure: Departure) : BottomSheetDialogFragment() {
+class DepartureBottomSheet(private var event: Event) : BottomSheetDialogFragment() {
 	companion object {
 		const val TAG = "DepartureBottomSheet"
 	}
@@ -338,33 +357,33 @@ 		cancelCallback?.let { it() }
 	}
 
 	fun departureID(): String {
-		return departure.ID
+		return event.id
 	}
 
-	fun update(departure: Departure) {
-		this.departure = departure
+	fun update(event: Event) {
+		this.event = event
 		view?.let { context?.let { ctx -> setContent(it, ctx, true) } }
 	}
 
 	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.time).text = event.arrivalTimeString(ctx)
 			findViewById<TextView>(R.id.local_time).visibility =
-				if (departure.time.Zone == ZoneId.systemDefault().id) {
+				if (event.timeZone() == ZoneId.systemDefault().id) {
 					View.GONE
 				} else {
 					View.VISIBLE
 				}
 
 			findViewById<ImageView>(R.id.rt_icon).apply {
-				visibility = if (departure.isRealtime) {
+				visibility = if (event.isRealtime) {
 					View.VISIBLE
 				} else {
 					View.GONE
 				}
 			}
 			findViewById<ImageView>(R.id.wheelchair_icon).apply {
-				visibility = if (departure.vehicle.let {
+				visibility = if (event.vehicle.let {
 						it.getCapability(Vehicle.Capability.LOW_FLOOR) || it.getCapability(Vehicle.Capability.LOW_ENTRY) || it.getCapability(
 							Vehicle.Capability.RAMP
 						)
@@ -378,18 +397,18 @@
 			findViewById<TextView>(R.id.line).apply {
 				contentDescription = getString(
 					R.string.vehicle_headsign_content_description,
-					departure.vehicle.Line.name,
-					departure.vehicle.Headsign
+					event.vehicle.Line.name,
+					event.vehicle.Headsign
 				)
 				text = getString(
 					R.string.vehicle_headsign,
-					departure.vehicle.Line.name,
-					departure.vehicle.Headsign
+					event.vehicle.Line.name,
+					event.vehicle.Headsign
 				)
 			}
 
 
-			departure.boardingText(ctx).let {
+			event.boardingText(ctx).let {
 				findViewById<TextView>(R.id.boarding_text).text = it
 				findViewById<ImageView>(R.id.boarding_icon).visibility = if (it == "") {
 					View.GONE
@@ -400,19 +419,19 @@ 			}
 			UnitSystem.getSelected(requireContext()).let { us ->
 				findViewById<TextView>(R.id.speed_text).apply {
 					text =
-						us.toString(context, us.speedUnit(departure.vehicle.Speed))
+						us.toString(context, us.speedUnit(event.vehicle.Speed))
 					contentDescription =
-						us.speedUnit(departure.vehicle.Speed).contentDescription(requireContext(), us.base)
+						us.speedUnit(event.vehicle.Speed).contentDescription(requireContext(), us.base)
 				}
 			}
 
 			findViewById<LinearLayout>(R.id.congestion).visibility =
-				if (departure.vehicle.congestionLevel == CongestionLevel.UNKNOWN) View.GONE else View.VISIBLE
-			findViewById<TextView>(R.id.congestion_text).text = departure.vehicle.congestion(ctx)
+				if (event.vehicle.congestionLevel == CongestionLevel.UNKNOWN) View.GONE else View.VISIBLE
+			findViewById<TextView>(R.id.congestion_text).text = event.vehicle.congestion(ctx)
 
 			findViewById<LinearLayout>(R.id.occupancy).visibility =
-				if (departure.vehicle.occupancyStatus == OccupancyStatus.UNKNOWN) View.GONE else View.VISIBLE
-			findViewById<TextView>(R.id.occupancy_text).text = departure.vehicle.occupancy(ctx)
+				if (event.vehicle.occupancyStatus == OccupancyStatus.UNKNOWN) View.GONE else View.VISIBLE
+			findViewById<TextView>(R.id.occupancy_text).text = event.vehicle.occupancy(ctx)
 
 			findViewById<ImageView>(R.id.ac).let {
 				TooltipCompat.setTooltipText(
@@ -420,7 +439,7 @@ 					it,
 					getString(R.string.air_condition_content_description)
 				)
 				it.visibility =
-					if (departure.vehicle.getCapability(Vehicle.Capability.AC)) View.VISIBLE else View.GONE
+					if (event.vehicle.getCapability(Vehicle.Capability.AC)) View.VISIBLE else View.GONE
 			}
 
 			findViewById<ImageView>(R.id.bike).let {
@@ -429,7 +448,7 @@ 					it,
 					getString(R.string.bicycles_allowed_content_description)
 				)
 				it.visibility =
-					if (departure.vehicle.getCapability(Vehicle.Capability.BIKE)) {
+					if (event.vehicle.getCapability(Vehicle.Capability.BIKE)) {
 						View.VISIBLE
 					} else {
 						View.GONE
@@ -442,7 +461,7 @@ 					it,
 					getString(R.string.voice_announcements_content_description)
 				)
 				it.visibility =
-					if (departure.vehicle.getCapability(Vehicle.Capability.VOICE)) {
+					if (event.vehicle.getCapability(Vehicle.Capability.VOICE)) {
 						View.VISIBLE
 					} else {
 						View.GONE
@@ -453,7 +472,7 @@ 				TooltipCompat.setTooltipText(
 					ticketImage,
 					getString(R.string.tickets_sold_content_description)
 				)
-				ticketImage.visibility = if (departure.vehicle.let {
+				ticketImage.visibility = if (event.vehicle.let {
 						it.getCapability(Vehicle.Capability.TICKET_DRIVER) || it.getCapability(Vehicle.Capability.TICKET_MACHINE)
 					}) {
 					View.VISIBLE
@@ -467,15 +486,15 @@ 					it,
 					getString(R.string.usb_charging_content_description)
 				)
 				it.visibility =
-					if (departure.vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) {
+					if (event.vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) {
 						View.VISIBLE
 					} else {
 						View.GONE
 					}
 			}
 
-			if (departure.alerts.isNotEmpty()) {
-				findViewById<MaterialTextView>(R.id.alerts_text).text = departure.alerts.map {
+			if (event.alerts.isNotEmpty()) {
+				findViewById<MaterialTextView>(R.id.alerts_text).text = event.alerts.map {
 					it.header.ifEmpty {
 						getString(R.string.alert_header)
 					}
@@ -486,7 +505,7 @@ 					setOnClickListener {
 						MaterialAlertDialogBuilder(context)
 							.setTitle(R.string.alerts)
 							.setPositiveButton(R.string.ok) { _, _ -> }
-							.setMessage(departure.alerts.map { it.description }.filter { it != "" }
+							.setMessage(event.alerts.map { it.description }.filter { it != "" }
 								.joinToString(separator = "\n"))
 							.show()
 					}
@@ -494,14 +513,14 @@ 				}
 			}
 
 			findViewById<MapView>(R.id.map).let { map ->
-				if (departure.vehicle.Position.isZero()) {
+				if (event.vehicle.Position.isZero()) {
 					map.visibility = View.GONE
 					return@let
 				}
 				map.controller.apply {
 					GeoPoint(
-						departure.vehicle.location().latitude,
-						departure.vehicle.location().longitude
+						event.vehicle.location().latitude,
+						event.vehicle.location().longitude
 					).let { geoPoint ->
 						if (updating) {
 							animateTo(
@@ -520,11 +539,11 @@ 				}
 				val marker = Marker(map).apply {
 					position =
 						GeoPoint(
-							departure.vehicle.location().latitude,
-							departure.vehicle.location().longitude
+							event.vehicle.location().latitude,
+							event.vehicle.location().longitude
 						)
 					setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
-					icon = context?.let { ctx -> departure.vehicle.icon(ctx, 2f) }
+					icon = context?.let { ctx -> event.vehicle.icon(ctx, 2f) }
 					setOnClickListener {}
 				}
 				map.overlays.add(marker)
@@ -546,7 +565,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/departures/DeparturesActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
index 0b725721dcfca87b8b52eaa06e4407b36784c410..7f6e164922dc8b74c2e0c234c26620031bd57f8a 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
@@ -45,7 +45,7 @@ import xyz.apiote.bimba.czwek.databinding.ActivityDeparturesBinding
 import xyz.apiote.bimba.czwek.departures.BimbaDeparturesAdapter.Companion.TERMINUS_ARRIVAL_GREY_OUT
 import xyz.apiote.bimba.czwek.departures.BimbaDeparturesAdapter.Companion.TERMINUS_ARRIVAL_HIDE
 import xyz.apiote.bimba.czwek.departures.BimbaDeparturesAdapter.Companion.TERMINUS_ARRIVAL_SHOWING_KEY
-import xyz.apiote.bimba.czwek.repo.DepartureItem
+import xyz.apiote.bimba.czwek.repo.EventItem
 import xyz.apiote.bimba.czwek.repo.Favourite
 import xyz.apiote.bimba.czwek.repo.OfflineRepository
 import xyz.apiote.bimba.czwek.repo.Stop
@@ -168,7 +168,7 @@ 			}
 
 		viewModel.linesFilter.observe(this) {
 			// TODO if is before we got departures, do nothing
-			val departures = viewModel.departures.value?.departures ?: emptyList()
+			val departures = viewModel.departures.value?.events ?: emptyList()
 			updateItems(departures
 				.filter { d ->
 					it.values.all { !it } or (it[d.vehicle.Line.name] ?: false)
@@ -177,20 +177,20 @@ 				.filter { d ->
 					viewModel.showingTerminusArrivals != TERMINUS_ARRIVAL_HIDE || !d.terminusArrival
 				}
 				.filter { d ->
-					val t = LocalTime.of(d.time.Hour.toInt(), d.time.Minute.toInt())
+					val t = LocalTime.of(d.filterTime().Hour.toInt(), d.filterTime().Minute.toInt())
 					t >= viewModel.startTime && t <= viewModel.endTime
-				}.map { DepartureItem(it) },
+				}.map { EventItem(it) },
 				null,
 				true
 			)
 		}
 
 		viewModel.departures.observe(this) { stopDepartures ->
-			val items = mutableListOf<DepartureItem>()
+			val items = mutableListOf<EventItem>()
 			if (stopDepartures.alerts.isNotEmpty()) {
-				items.add(DepartureItem(stopDepartures.alerts))
+				items.add(EventItem(stopDepartures.alerts))
 			}
-			items.addAll(stopDepartures.departures
+			items.addAll(stopDepartures.events
 				.filter { d ->
 					viewModel.linesFilter.value?.let { filter ->
 						filter.values.all { !it } or (filter[d.vehicle.Line.name] ?: false)
@@ -200,15 +200,15 @@ 				.filter { d ->
 					viewModel.showingTerminusArrivals != TERMINUS_ARRIVAL_HIDE || !d.terminusArrival
 				}
 				.filter { d ->
-					val t = LocalTime.of(d.time.Hour.toInt(), d.time.Minute.toInt())
+					val t = LocalTime.of(d.filterTime().Hour.toInt(), d.filterTime().Minute.toInt())
 					t >= viewModel.startTime && t <= viewModel.endTime
-				}.map { DepartureItem(it) })
+				}.map { EventItem(it) })
 			updateItems(items, stopDepartures.stop)
 			viewModel.openBottomSheet?.departureID()?.let { adapter.get(it) }
-				?.let { it.departure?.let { departure -> viewModel.openBottomSheet?.update(departure) } }
+				?.let { it.event?.let { departure -> viewModel.openBottomSheet?.update(departure) } }
 
 
-			val lines = stopDepartures.departures.map { it.vehicle.Line.name }.sortedWith { s1, s2 ->
+			val lines = stopDepartures.events.map { it.vehicle.Line.name }.sortedWith { s1, s2 ->
 				val s1n = s1.toIntOrNull()
 				val s2n = s2.toIntOrNull()
 				if (s1n != null && s2n != null) {
@@ -518,7 +518,7 @@ 		binding.errorText.visibility = View.GONE
 	}
 
 	private fun updateItems(
-		departures: List<DepartureItem>,
+		departures: List<EventItem>,
 		stop: Stop?,
 		leaveAlert: Boolean = false
 	) {




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt
index bd61194560e6a232a5754bbf33fa60b3ed03dc6f..21dbd04eefa862f93ad0f5ad95a0fc1ef48e80db 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt
@@ -20,14 +20,14 @@ import xyz.apiote.bimba.czwek.repo.FeedInfo
 import xyz.apiote.bimba.czwek.repo.OfflineRepository
 import xyz.apiote.bimba.czwek.repo.OnlineRepository
 import xyz.apiote.bimba.czwek.repo.QrLocation
-import xyz.apiote.bimba.czwek.repo.StopDepartures
+import xyz.apiote.bimba.czwek.repo.StopEvents
 import xyz.apiote.bimba.czwek.repo.TrafficResponseException
 import java.time.LocalDate
 import java.time.LocalTime
 
 class DeparturesViewModel : ViewModel() {
-	private val _departures = MutableLiveData<StopDepartures>()
-	val departures: LiveData<StopDepartures> = _departures
+	private val _departures = MutableLiveData<StopEvents>()
+	val departures: LiveData<StopEvents> = _departures
 	private val _error = MutableLiveData<Error>()
 	val error: LiveData<Error> = _error
 	var requestedItemsNumber = 12
@@ -64,7 +64,7 @@ 						context,
 						requestedItemsNumber
 					)
 				stopDepartures?.let {
-					if (stopDepartures.departures.isEmpty()) {
+					if (stopDepartures.events.isEmpty()) {
 						val (string, image) = mapHttpError(44)
 						throw TrafficResponseException(44, "", Error(44, string, image))
 					}




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
deleted file mode 100644
index 38a48a946c2048ad2fa19b5eb4fa169769287adc..0000000000000000000000000000000000000000
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
+++ /dev/null
@@ -1,242 +0,0 @@
-// SPDX-FileCopyrightText: Adam Evyčędo
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package xyz.apiote.bimba.czwek.repo
-
-import android.content.Context
-import android.text.format.DateUtils
-import xyz.apiote.bimba.czwek.R
-import xyz.apiote.bimba.czwek.api.AlertCauseV1
-import xyz.apiote.bimba.czwek.api.AlertEffectV1
-import xyz.apiote.bimba.czwek.api.AlertV1
-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
-import xyz.apiote.bimba.czwek.units.TGM
-import xyz.apiote.bimba.czwek.units.UnitSystem
-import java.time.Instant
-import java.time.ZoneId
-import java.time.ZonedDateTime
-import java.time.format.DateTimeFormatter
-import java.time.temporal.ChronoUnit
-
-
-class DepartureItem {
-	private constructor(d: Departure?, a: List<Alert>) {
-		departure = d
-		alert = a
-	}
-
-	constructor(d: Departure) : this(d, emptyList())
-	constructor(a: List<Alert>) : this(null, a)
-
-	val departure: Departure?
-	val alert: List<Alert>
-}
-
-enum class AlertCause {
-	UNKNOWN, OTHER, TECHNICAL_PROBLEM, STRIKE, DEMONSTRATION, ACCIDENT, HOLIDAY, WEATHER, MAINTENANCE,
-	CONSTRUCTION, POLICE_ACTIVITY, MEDICAL_EMERGENCY;
-
-	companion object {
-		fun of(type: AlertCauseV1): AlertCause {
-			return when (type) {
-				AlertCauseV1.UNKNOWN -> valueOf("UNKNOWN")
-				AlertCauseV1.OTHER -> valueOf("OTHER")
-				AlertCauseV1.TECHNICAL_PROBLEM -> valueOf("TECHNICAL_PROBLEM")
-				AlertCauseV1.STRIKE -> valueOf("STRIKE")
-				AlertCauseV1.DEMONSTRATION -> valueOf("DEMONSTRATION")
-				AlertCauseV1.ACCIDENT -> valueOf("ACCIDENT")
-				AlertCauseV1.HOLIDAY -> valueOf("HOLIDAY")
-				AlertCauseV1.WEATHER -> valueOf("WEATHER")
-				AlertCauseV1.MAINTENANCE -> valueOf("MAINTENANCE")
-				AlertCauseV1.CONSTRUCTION -> valueOf("CONSTRUCTION")
-				AlertCauseV1.POLICE_ACTIVITY -> valueOf("POLICE_ACTIVITY")
-				AlertCauseV1.MEDICAL_EMERGENCY -> valueOf("MEDICAL_EMERGENCY")
-			}
-		}
-	}
-}
-
-enum class AlertEffect {
-	UNKNOWN, OTHER, NO_SERVICE, REDUCED_SERVICE, SIGNIFICANT_DELAYS, DETOUR, ADDITIONAL_SERVICE,
-	MODIFIED_SERVICE, STOP_MOVED, NONE, ACCESSIBILITY_ISSUE;
-
-	companion object {
-		fun of(type: AlertEffectV1): AlertEffect {
-			return when (type) {
-				AlertEffectV1.UNKNOWN -> valueOf("UNKNOWN")
-				AlertEffectV1.OTHER -> valueOf("OTHER")
-				AlertEffectV1.NO_SERVICE -> valueOf("NO_SERVICE")
-				AlertEffectV1.REDUCED_SERVICE -> valueOf("REDUCED_SERVICE")
-				AlertEffectV1.SIGNIFICANT_DELAYS -> valueOf("SIGNIFICANT_DELAYS")
-				AlertEffectV1.DETOUR -> valueOf("DETOUR")
-				AlertEffectV1.ADDITIONAL_SERVICE -> valueOf("ADDITIONAL_SERVICE")
-				AlertEffectV1.MODIFIED_SERVICE -> valueOf("MODIFIED_SERVICE")
-				AlertEffectV1.STOP_MOVED -> valueOf("STOP_MOVED")
-				AlertEffectV1.NONE -> valueOf("NONE")
-				AlertEffectV1.ACCESSIBILITY_ISSUE -> valueOf("ACCESSIBILITY_ISSUE")
-			}
-		}
-	}
-}
-
-data class Alert(
-	val header: String,
-	val description: String,
-	val url: String,
-	val cause: AlertCause,
-	val effect: AlertEffect
-) {
-	constructor(a: AlertV1) : this(
-		a.header,
-		a.Description,
-		a.Url,
-		AlertCause.of(a.Cause),
-		AlertEffect.of(a.Effect)
-	)
-}
-
-data class StopDepartures(
-	val departures: List<Departure>,
-	val stop: Stop,
-	val alerts: List<Alert>
-)
-
-data class Departure(
-	val ID: String,
-	val time: Time,
-	val status: ULong,
-	val isRealtime: Boolean,
-	val vehicle: Vehicle,
-	val boarding: UByte,
-	val alerts: List<Alert>,
-	val exact: Boolean,
-	val terminusArrival: Boolean
-) {
-
-	constructor(d: DepartureV1) : this(
-		d.ID,
-		d.time,
-		d.status,
-		d.isRealtime,
-		Vehicle(d.vehicle),
-		d.boarding,
-		emptyList(),
-		true,
-		false
-	)
-
-	constructor(d: DepartureV2) : this(
-		d.ID,
-		d.time,
-		d.status,
-		d.isRealtime,
-		Vehicle(d.vehicle),
-		d.boarding,
-		emptyList(),
-		true,
-		false
-	)
-
-	constructor(d: DepartureV3) : this(
-		d.ID,
-		d.time,
-		d.status.ordinal.toULong(), // TODO VehicleStatus
-		d.isRealtime,
-		Vehicle(d.vehicle),
-		d.boarding,
-		emptyList(),
-		true,
-		false
-	)
-
-	constructor(d: DepartureV4) : this(
-		d.ID,
-		d.time,
-		d.status.ordinal.toULong(), // TODO VehicleStatus
-		d.isRealtime,
-		Vehicle(d.vehicle),
-		d.boarding,
-		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 {
-		val now = at ?: Instant.now().atZone(ZoneId.systemDefault())
-		val departureTime = ZonedDateTime.of(
-			now.year, now.monthValue, now.dayOfMonth,
-			time.Hour.toInt(), time.Minute.toInt(), time.Second.toInt(), 0, ZoneId.of(time.Zone)
-		).plus(time.DayOffset.toLong(), ChronoUnit.DAYS)
-		if (showAsTime) {
-			return departureTime.format(DateTimeFormatter.ofPattern("HH:mm"))
-		}
-		var r = status.toUInt()
-		if (departureTime.isBefore(now) && r < 3u) {
-			r = 0u
-		}
-		return when (r) {
-			0u -> if (context != null && UnitSystem.getSelected(context) is TGM) {
-				val us = UnitSystem.getSelected(context)
-				us.toString(
-					context,
-					us.timeUnit(Second((departureTime.toEpochSecond() - now.toEpochSecond()).toInt()))
-				)
-			} else {
-				DateUtils.getRelativeTimeSpanString(
-					departureTime.toEpochSecond() * 1000,
-					now.toEpochSecond() * 1000,
-					DateUtils.MINUTE_IN_MILLIS,
-					DateUtils.FORMAT_ABBREV_RELATIVE
-				).toString()
-			}
-
-			1u -> context?.getString(R.string.departure_momentarily) ?: "momentarily"
-			2u -> context?.getString(R.string.departure_now) ?: "now"
-			3u -> context?.getString(R.string.departure_departed) ?: "departed"
-			else -> throw UnknownResourceVersionException("VehicleStatus/$r", 1u)
-		}
-	}
-
-	fun timeString(context: Context): String {
-		return when {
-			isRealtime -> context.getString(
-				R.string.at_time_realtime, time.Hour.toInt(), time.Minute.toInt(), time.Second.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())
-		}
-	}
-
-	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)
-			else -> context.getString(R.string.on_demand)
-		}
-	}
-}




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Event.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Event.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6fad703e8548c4544c9b53d3406a61cd173691d2
--- /dev/null
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Event.kt
@@ -0,0 +1,294 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package xyz.apiote.bimba.czwek.repo
+
+import android.content.Context
+import android.text.format.DateUtils
+import xyz.apiote.bimba.czwek.R
+import xyz.apiote.bimba.czwek.api.AlertCauseV1
+import xyz.apiote.bimba.czwek.api.AlertEffectV1
+import xyz.apiote.bimba.czwek.api.AlertV1
+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
+import xyz.apiote.bimba.czwek.units.TGM
+import xyz.apiote.bimba.czwek.units.UnitSystem
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoUnit
+
+
+class EventItem {
+	private constructor(d: Event?, a: List<Alert>) {
+		event = d
+		alert = a
+	}
+
+	constructor(d: Event) : this(d, emptyList())
+	constructor(a: List<Alert>) : this(null, a)
+
+	val event: Event?
+	val alert: List<Alert>
+}
+
+enum class AlertCause {
+	UNKNOWN, OTHER, TECHNICAL_PROBLEM, STRIKE, DEMONSTRATION, ACCIDENT, HOLIDAY, WEATHER, MAINTENANCE,
+	CONSTRUCTION, POLICE_ACTIVITY, MEDICAL_EMERGENCY;
+
+	companion object {
+		fun of(type: AlertCauseV1): AlertCause {
+			return when (type) {
+				AlertCauseV1.UNKNOWN -> valueOf("UNKNOWN")
+				AlertCauseV1.OTHER -> valueOf("OTHER")
+				AlertCauseV1.TECHNICAL_PROBLEM -> valueOf("TECHNICAL_PROBLEM")
+				AlertCauseV1.STRIKE -> valueOf("STRIKE")
+				AlertCauseV1.DEMONSTRATION -> valueOf("DEMONSTRATION")
+				AlertCauseV1.ACCIDENT -> valueOf("ACCIDENT")
+				AlertCauseV1.HOLIDAY -> valueOf("HOLIDAY")
+				AlertCauseV1.WEATHER -> valueOf("WEATHER")
+				AlertCauseV1.MAINTENANCE -> valueOf("MAINTENANCE")
+				AlertCauseV1.CONSTRUCTION -> valueOf("CONSTRUCTION")
+				AlertCauseV1.POLICE_ACTIVITY -> valueOf("POLICE_ACTIVITY")
+				AlertCauseV1.MEDICAL_EMERGENCY -> valueOf("MEDICAL_EMERGENCY")
+			}
+		}
+	}
+}
+
+enum class AlertEffect {
+	UNKNOWN, OTHER, NO_SERVICE, REDUCED_SERVICE, SIGNIFICANT_DELAYS, DETOUR, ADDITIONAL_SERVICE,
+	MODIFIED_SERVICE, STOP_MOVED, NONE, ACCESSIBILITY_ISSUE;
+
+	companion object {
+		fun of(type: AlertEffectV1): AlertEffect {
+			return when (type) {
+				AlertEffectV1.UNKNOWN -> valueOf("UNKNOWN")
+				AlertEffectV1.OTHER -> valueOf("OTHER")
+				AlertEffectV1.NO_SERVICE -> valueOf("NO_SERVICE")
+				AlertEffectV1.REDUCED_SERVICE -> valueOf("REDUCED_SERVICE")
+				AlertEffectV1.SIGNIFICANT_DELAYS -> valueOf("SIGNIFICANT_DELAYS")
+				AlertEffectV1.DETOUR -> valueOf("DETOUR")
+				AlertEffectV1.ADDITIONAL_SERVICE -> valueOf("ADDITIONAL_SERVICE")
+				AlertEffectV1.MODIFIED_SERVICE -> valueOf("MODIFIED_SERVICE")
+				AlertEffectV1.STOP_MOVED -> valueOf("STOP_MOVED")
+				AlertEffectV1.NONE -> valueOf("NONE")
+				AlertEffectV1.ACCESSIBILITY_ISSUE -> valueOf("ACCESSIBILITY_ISSUE")
+			}
+		}
+	}
+}
+
+data class Alert(
+	val header: String,
+	val description: String,
+	val url: String,
+	val cause: AlertCause,
+	val effect: AlertEffect
+) {
+	constructor(a: AlertV1) : this(
+		a.header,
+		a.Description,
+		a.Url,
+		AlertCause.of(a.Cause),
+		AlertEffect.of(a.Effect)
+	)
+}
+
+data class StopEvents(
+	val events: List<Event>,
+	val stop: Stop,
+	val alerts: List<Alert>
+)
+
+data class Event(
+	val id: String,
+	val arrivalTime: Time?,
+	val departureTime: Time?,
+	val status: ULong,
+	val isRealtime: Boolean,
+	val vehicle: Vehicle,
+	val boarding: UByte,
+	val alerts: List<Alert>,
+	val exact: Boolean,
+	val terminusArrival: Boolean
+) {
+
+	constructor(d: DepartureV1) : this(
+		d.ID,
+		d.time,
+		d.time,
+		d.status,
+		d.isRealtime,
+		Vehicle(d.vehicle),
+		d.boarding,
+		emptyList(),
+		true,
+		false
+	)
+
+	constructor(d: DepartureV2) : this(
+		d.ID,
+		d.time,
+		d.time,
+		d.status,
+		d.isRealtime,
+		Vehicle(d.vehicle),
+		d.boarding,
+		emptyList(),
+		true,
+		false
+	)
+
+	constructor(d: DepartureV3) : this(
+		d.ID,
+		d.time,
+		d.time,
+		d.status.ordinal.toULong(), // TODO VehicleStatus
+		d.isRealtime,
+		Vehicle(d.vehicle),
+		d.boarding,
+		emptyList(),
+		true,
+		false
+	)
+
+	constructor(d: DepartureV4) : this(
+		d.ID,
+		d.time,
+		d.time,
+		d.status.ordinal.toULong(), // TODO VehicleStatus
+		d.isRealtime,
+		Vehicle(d.vehicle),
+		d.boarding,
+		d.alerts.map { Alert(it) },
+		true,
+		false
+	)
+
+	constructor(d: DepartureV5) : this(
+		d.ID,
+		d.time,
+		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 timeZone() = (arrivalTime?:departureTime)!!.Zone
+
+	fun filterTime() = (arrivalTime?:departureTime)!!
+
+	fun statusText(
+		context: Context?,
+		showAsTime: Boolean,
+		at: ZonedDateTime? = null
+	): Pair<String?, String?> {
+		val now = at ?: Instant.now().atZone(ZoneId.systemDefault())
+		return Pair(statusText(context, showAsTime, now, arrivalTime, R.string.departure_arrived), statusText(context, showAsTime, now, departureTime, R.string.departure_departed))
+	}
+
+	private fun statusText(context: Context?, showAsTime: Boolean, now: ZonedDateTime, time: Time?, pastString: Int): String? {
+		val r = status.toUInt()
+		return time?.let {
+			ZonedDateTime.of(
+				now.year,
+				now.monthValue,
+				now.dayOfMonth,
+				it.Hour.toInt(),
+				it.Minute.toInt(),
+				it.Second.toInt(),
+				0,
+				ZoneId.of(
+					it.Zone
+				)
+			)
+				.plus(it.DayOffset.toLong(), ChronoUnit.DAYS)
+				.apply {
+					if (showAsTime) {
+						format(DateTimeFormatter.ofPattern("HH:mm"))
+					}
+				}.let {
+					when {
+						// TODO why this condition
+						r == 0u || (it.isBefore(now) && r < 3u) -> if (context != null && UnitSystem.getSelected(
+								context
+							) is TGM
+						) {
+							val us = UnitSystem.getSelected(context)
+							us.toString(
+								context,
+								us.timeUnit(Second((it.toEpochSecond() - now.toEpochSecond()).toInt()))
+							)
+						} else {
+							DateUtils.getRelativeTimeSpanString(
+								it.toEpochSecond() * 1000,
+								now.toEpochSecond() * 1000,
+								DateUtils.MINUTE_IN_MILLIS,
+								DateUtils.FORMAT_ABBREV_RELATIVE
+							).toString()
+						}
+
+						r == 1u -> context?.getString(R.string.departure_momentarily) ?: "momentarily"
+						r == 2u -> context?.getString(R.string.departure_now) ?: "now"
+						r == 3u -> context?.getString(pastString) ?: "passed"
+						else -> throw UnknownResourceVersionException("VehicleStatus/$r", 1u)
+					}
+				}
+		}
+
+	}
+
+	fun departureTimeString(context: Context): String? = timeString(context, departureTime)
+
+	fun arrivalTimeString(context: Context): String? = timeString(context, arrivalTime)
+
+	private fun timeString(context: Context, time: Time?): String? {
+		return when {
+			time == null -> null
+			isRealtime -> context.getString(
+				R.string.at_time_realtime,
+				time.Hour.toInt(),
+				time.Minute.toInt(),
+				time.Second.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()
+			)
+		}
+
+	}
+
+	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)
+			else -> context.getString(R.string.on_demand)
+		}
+	}
+}




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 a337b0178c144f066ffea44ebd3898c475d850c0..e612503b10f270a49961d1482475da55fb3f47db 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
@@ -36,7 +36,7 @@ 		stop: String,
 		date: LocalDate?,
 		context: Context,
 		limit: Int?
-	): StopDepartures?
+	): StopEvents?
 
 	suspend fun getLocatablesIn(
 		context: Context,




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 b11a5dfcccc9048e0390e0dfe9ad530e875a7668..309d6ec0f00248d543db51e92389dfddb1457a3d 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
@@ -200,7 +200,7 @@ 		stop: String,
 		date: LocalDate?,
 		context: Context,
 		limit: Int?
-	): StopDepartures? {
+	): StopEvents? {
 		TODO("Not yet implemented")
 	}
 




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 89f7f8d5ccc84cde130f7c6a92ded75a16e30170..0b47623ff0b1f191b90a9e19f308f9a50878117f 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
@@ -114,7 +114,7 @@ 		stop: String,
 		date: LocalDate?,
 		context: Context,
 		limit: Int?
-	): StopDepartures? {
+	): StopEvents? {
 		return if (feedID == "transitous") {
 			getTransitousDepartures(context, stop, date, limit)
 		} else {
@@ -137,28 +137,28 @@ 				}
 			} else {
 				when (val response =
 					withContext(Dispatchers.IO) { DeparturesResponse.unmarshal(result.stream!!) }) {
-					is DeparturesResponseDev -> StopDepartures(
-						response.departures.map { Departure(it) },
+					is DeparturesResponseDev -> StopEvents(
+						response.departures.map { Event(it) },
 						Stop(response.stop),
 						response.alerts.map { Alert(it) })
 
-					is DeparturesResponseV4 -> StopDepartures(
-						response.departures.map { Departure(it) },
+					is DeparturesResponseV4 -> StopEvents(
+						response.departures.map { Event(it) },
 						Stop(response.stop),
 						response.alerts.map { Alert(it) })
 
-					is DeparturesResponseV3 -> StopDepartures(
-						response.departures.map { Departure(it) },
+					is DeparturesResponseV3 -> StopEvents(
+						response.departures.map { Event(it) },
 						Stop(response.stop),
 						response.alerts.map { Alert(it) })
 
-					is DeparturesResponseV2 -> StopDepartures(
-						response.departures.map { Departure(it) },
+					is DeparturesResponseV2 -> StopEvents(
+						response.departures.map { Event(it) },
 						Stop(response.stop),
 						response.alerts.map { Alert(it) })
 
-					is DeparturesResponseV1 -> StopDepartures(
-						response.departures.map { Departure(it) },
+					is DeparturesResponseV1 -> StopEvents(
+						response.departures.map { Event(it) },
 						Stop(response.stop),
 						response.alerts.map { Alert(it) })
 




diff --git a/app/src/main/res/layout/departure.xml b/app/src/main/res/layout/departure.xml
index b2514262349aef0d448fed775a6116b4042ece6d..0e0e259af966194135137bef7b3438927d3699c3 100644
--- a/app/src/main/res/layout/departure.xml
+++ b/app/src/main/res/layout/departure.xml
@@ -7,63 +7,100 @@ SPDX-License-Identifier: GPL-3.0-or-later
 -->
 
 <androidx.constraintlayout.widget.ConstraintLayout 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"
-	android:id="@+id/departure"
-	android:layout_width="match_parent"
-	android:layout_height="wrap_content">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tool="http://schemas.android.com/tools"
+    android:id="@+id/departure"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <ImageView
+        android:id="@+id/line_icon"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_marginStart="8dp"
+        app:layout_constraintBottom_toTopOf="@+id/departure_headsign"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/arrival_time"
+        tool:ignore="ContentDescription"
+        tool:srcCompat="@drawable/bus_black" />
+
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/arrival_status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tool:text="approx. arr." />
+
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/arrival_time"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/arrival_status"
+        tool:text="1hr" />
 
-	<ImageView
-		android:id="@+id/line_icon"
-		android:layout_width="24dp"
-		android:layout_height="24dp"
-		android:layout_marginStart="8dp"
-		app:layout_constraintBottom_toTopOf="@+id/departure_headsign"
-		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toTopOf="@+id/departure_time"
-		tool:srcCompat="@drawable/bus_black"
-		tool:ignore="ContentDescription" />
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/departure_status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/arrival_time"
+        tool:text="approx. dep." />
 
-	<com.google.android.material.textview.MaterialTextView
-		android:id="@+id/departure_time"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_marginTop="8dp"
-		android:layout_marginEnd="8dp"
-		android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
-		app:layout_constraintEnd_toEndOf="parent"
-		app:layout_constraintTop_toTopOf="parent"
-		tool:text="1hr" />
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/departure_time"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/departure_status"
+        tool:text="1hr" />
 
-	<com.google.android.material.textview.MaterialTextView
-		android:id="@+id/departure_line"
-		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"
-		app:layout_constraintEnd_toStartOf="@id/departure_time"
-		tool:text="Circle" />
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/departure_line"
+        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_constraintEnd_toStartOf="@id/arrival_time"
+        app:layout_constraintStart_toEndOf="@+id/line_icon"
+        app:layout_constraintTop_toTopOf="parent"
+        tool:text="Circle" />
 
-	<com.google.android.material.textview.MaterialTextView
-		android:id="@+id/departure_headsign"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:textAppearance="@style/TextAppearance.Material3.BodySmall"
-		app:layout_constraintStart_toStartOf="@+id/departure_line"
-		app:layout_constraintTop_toBottomOf="@+id/departure_line"
-		tool:text="» Tower Hill" />
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/departure_headsign"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/TextAppearance.Material3.BodySmall"
+        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" />
+    <ImageView
+        android:id="@+id/event_status"
+        android:layout_width="15dp"
+        android:layout_height="11dp"
+        android:layout_marginStart="8dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginBottom="8dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/departure_headsign"
+        app:layout_constraintVertical_bias="0.0"
+        tool:ignore="ContentDescription"
+        tool:srcCompat="@drawable/radar" />
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout/favourite.xml b/app/src/main/res/layout/favourite.xml
index 7d47ac03dc672895ee185d4c471ffb19cf4847b7..e6d8ea9e5c3dbbd3ba471413765bea8240f1cf2e 100644
--- a/app/src/main/res/layout/favourite.xml
+++ b/app/src/main/res/layout/favourite.xml
@@ -45,12 +45,12 @@ 			android:layout_height="24dp"
 			android:layout_marginStart="8dp"
 			app:layout_constraintBottom_toTopOf="@+id/departure_headsign"
 			app:layout_constraintStart_toStartOf="parent"
-			app:layout_constraintTop_toTopOf="@+id/departure_time"
+			app:layout_constraintTop_toTopOf="@+id/arrival_time"
 			tool:ignore="ContentDescription"
 			tool:srcCompat="@drawable/bus_black" />
 
 		<com.google.android.material.textview.MaterialTextView
-			android:id="@+id/departure_time"
+			android:id="@+id/arrival_time"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
 			android:layout_marginEnd="8dp"
@@ -65,8 +65,8 @@ 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
 			android:layout_marginTop="4dp"
 			android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
-			app:layout_constraintEnd_toEndOf="@+id/departure_time"
-			app:layout_constraintTop_toBottomOf="@+id/departure_time"
+			app:layout_constraintEnd_toEndOf="@+id/arrival_time"
+			app:layout_constraintTop_toBottomOf="@+id/arrival_time"
 			tool:text="18:55" />
 
 		<com.google.android.material.textview.MaterialTextView




diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f0881e76ecbc33476956161aa61f7919fde2a3d0..6ddbc228a9ae386902163571a89eea12cb8ad0e0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -149,9 +149,7 @@ 	Server
 	<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="schedule_content_description">departure time is 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>
@@ -288,4 +286,9 @@ 	link to email
 	<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>
+    <string name="departure_arrived">arrived</string>
+	<string name="arrival_approximate">approx. arr.</string>
+	<string name="arrival">arrival</string>
+	<string name="departure_approximate">approx. dep.</string>
+	<string name="departure">departure</string>
 </resources>




diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml
index e39bfc51ee91e735e1e5a29982850c5b76cfddd5..f20916cb26b056ffd7ed22c61aed616a0e33d316 100644
--- a/app/src/main/res/values-en-rGB/strings.xml
+++ b/app/src/main/res/values-en-rGB/strings.xml
@@ -147,9 +147,7 @@ 	Server
 	<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="schedule_content_description">departure time is 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>
@@ -286,4 +284,8 @@ 	link to email
 	<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>
+    <string name="departure_arrived">arrived</string>
+	<string name="arrival_approximate">approx. arr.</string>
+	<string name="departure_approximate">approx. dep.</string>
+	<string name="departure">departure</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 e41b22e601cc5d006112aa47cb3f0a9ea341921a..5bec455c4cb9b41af902e598c8062810869a6359 100644
--- a/app/src/main/res/values-en-rUS/strings.xml
+++ b/app/src/main/res/values-en-rUS/strings.xml
@@ -103,8 +103,7 @@ 	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>
-	<string name="exact_content_description">departure time is exact from schedule</string>
-	<string name="inexact_content_description">departure time is approximate</string>
+	<string name="schedule_content_description">departure time is 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>
@@ -283,4 +282,8 @@ 	No email app installed
 	<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>
+    <string name="departure_arrived">arrived</string>
+	<string name="arrival_approximate">approx. arr.</string>
+	<string name="departure_approximate">approx. dep.</string>
+	<string name="departure">departure</string>
 </resources>
\ No newline at end of file




diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index a583c09cc1089d2f2f972a9b0596380c0a308b7e..83a1b8c9213f2cc3b1fb6e8128fa246b3ebb001e 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -64,8 +64,7 @@     Wyniki dla „%1$s”
     <string name="bimba_server_address_hint">Serwer</string>
     <string name="bimba_server_token_hint">Żeton</string>
     <string name="realtime_content_description">odjazd w czasie rzeczywistym</string>
-    <string name="exact_content_description">czas odjazdu jest dokładny z rozkładu</string>
-    <string name="inexact_content_description">czas odjazdu jest przybliżony</string>
+    <string name="schedule_content_description">czas odjazdu jest z rozkładu</string>
     <string name="wheelchair_content_description">pojazd ma niską podłogę</string>
     <string name="air_condition_content_description">klimatyzacja</string>
     <string name="bicycles_allowed_content_description">przewóz rowerów dozwolony</string>