Bimba.git

commit 162c109c91210d0b4afbe6279141cd6d0b5f3ea8

Author: Adam <git@apiote.xyz>

support departure alerts

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


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 5a1468c35018ec56820e55c0129b1983e8aaf534..426f7aadef379bb756e4d90be674f0d67ab83de8 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
@@ -456,6 +456,35 @@ 		}
 	}
 }
 
+data class DepartureV4(
+	val ID: String,
+	val time: Time,
+	val status: VehicleStatusV1,
+	val isRealtime: Boolean,
+	val vehicle: VehicleV3,
+	val boarding: UByte,
+	val alerts: List<AlertV1>
+) {
+
+	companion object {
+		fun unmarshal(stream: InputStream): DepartureV4 {
+			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))
+			}
+			return DepartureV4(id, time, status, isRealtime, vehicle, boarding, alerts)
+		}
+	}
+}
+
 @Parcelize
 data class StopV2(
 	val code: String,




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 ea9d6279fd223e940cb9df6f90889e221c2e435d..330be67791b036ebac516b1802c0a8c1f5e3ec2e 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
@@ -8,9 +8,9 @@ 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.StopV1
 import xyz.apiote.bimba.czwek.api.StopV2
-import xyz.apiote.bimba.czwek.api.responses.UnknownResponseVersion
 import xyz.apiote.fruchtfleisch.Reader
 import java.io.InputStream
 
@@ -31,13 +31,13 @@ }
 
 data class DeparturesResponseDev(
 	val alerts: List<AlertV1>,
-	val departures: List<DepartureV3>,
+	val departures: List<DepartureV4>,
 	val stop: StopV2
 ) : DeparturesResponse {
 	companion object {
 		fun unmarshal(stream: InputStream): DeparturesResponseDev {
 			val alerts = mutableListOf<AlertV1>()
-			val departures = mutableListOf<DepartureV3>()
+			val departures = mutableListOf<DepartureV4>()
 
 			val reader = Reader(stream)
 			val alertsNum = reader.readUInt().toULong()
@@ -47,7 +47,7 @@ 				alerts.add(alert)
 			}
 			val departuresNum = reader.readUInt().toULong()
 			for (i in 0UL until departuresNum) {
-				val departure = DepartureV3.unmarshal(stream)
+				val departure = DepartureV4.unmarshal(stream)
 				departures.add(departure)
 			}
 
@@ -62,7 +62,7 @@ 	val departures: List,
 	val stop: StopV2
 ) : DeparturesResponse {
 	companion object {
-		fun unmarshal(stream: InputStream): DeparturesResponseDev {
+		fun unmarshal(stream: InputStream): DeparturesResponseV3 {
 			val alerts = mutableListOf<AlertV1>()
 			val departures = mutableListOf<DepartureV3>()
 
@@ -78,7 +78,7 @@ 				val departure = DepartureV3.unmarshal(stream)
 				departures.add(departure)
 			}
 
-			return DeparturesResponseDev(alerts, departures, StopV2.unmarshal(stream))
+			return DeparturesResponseV3(alerts, departures, StopV2.unmarshal(stream))
 		}
 	}
 }




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 b42ef754e0fa53865187fc849c90534c58f318ff..2869aa89f82c9be9f49bd2def90c97062754b177 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
@@ -17,10 +17,12 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
 import android.widget.TextView
+import androidx.appcompat.widget.TooltipCompat
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.RecyclerView
 import com.google.android.material.bottomsheet.BottomSheetDialog
 import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.textview.MaterialTextView
 import org.osmdroid.tileprovider.tilesource.TileSourceFactory
 import org.osmdroid.util.GeoPoint
 import org.osmdroid.views.CustomZoomButtonsController
@@ -30,7 +32,9 @@ import org.osmdroid.views.overlay.TilesOverlay
 import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.dpToPixelI
+import xyz.apiote.bimba.czwek.repo.CongestionLevel
 import xyz.apiote.bimba.czwek.repo.Departure
+import xyz.apiote.bimba.czwek.repo.OccupancyStatus
 import xyz.apiote.bimba.czwek.repo.Vehicle
 import java.time.ZoneId
 import java.time.ZonedDateTime
@@ -215,41 +219,90 @@ 			findViewById(R.id.boarding_text).text = departure.boardingText(ctx)
 			// todo units -- [3.2] settings or system-based
 			findViewById<TextView>(R.id.speed_text).text =
 				getString(R.string.speed_in_km_per_h, departure.vehicle.Speed * 3.6)
-			findViewById<TextView>(R.id.congestion_text).text = departure.vehicle.congestion(ctx)
-			findViewById<TextView>(R.id.occupancy_text).text = departure.vehicle.occupancy(ctx)
+
+			findViewById<TextView>(R.id.congestion_text).let {
+				it.visibility =
+					if (departure.vehicle.congestionLevel == CongestionLevel.UNKNOWN) View.INVISIBLE else View.VISIBLE
+				it.text = departure.vehicle.congestion(ctx)
+			}
+			findViewById<TextView>(R.id.congestion_icon).visibility =
+				if (departure.vehicle.congestionLevel == CongestionLevel.UNKNOWN) View.INVISIBLE else View.VISIBLE
+
+			findViewById<TextView>(R.id.occupancy_text).let {
+				it.visibility =
+					if (departure.vehicle.occupancyStatus == OccupancyStatus.UNKNOWN) View.INVISIBLE else View.VISIBLE
+				it.text = departure.vehicle.occupancy(ctx)
+			}
+			findViewById<TextView>(R.id.occupancy_icon).visibility =
+				if (departure.vehicle.occupancyStatus == OccupancyStatus.UNKNOWN) View.INVISIBLE else View.VISIBLE
+
+			findViewById<ImageView>(R.id.ac).let {
+				TooltipCompat.setTooltipText(
+					it,
+					getString(R.string.air_condition_content_description)
+				)
+				it.visibility =
+					if (departure.vehicle.getCapability(Vehicle.Capability.AC)) View.VISIBLE else View.GONE
+			}
+
+			findViewById<ImageView>(R.id.bike).let {
+				TooltipCompat.setTooltipText(
+					it,
+					getString(R.string.bicycles_allowed_content_description)
+				)
+				it.visibility =
+					if (departure.vehicle.getCapability(Vehicle.Capability.BIKE)) {
+						View.VISIBLE
+					} else {
+						View.GONE
+					}
+			}
 
-			findViewById<ImageView>(R.id.ac).visibility =
-				if (departure.vehicle.getCapability(Vehicle.Capability.AC)) {
-					View.VISIBLE
-				} else {
-					View.GONE
-				}
-			findViewById<ImageView>(R.id.bike).visibility =
-				if (departure.vehicle.getCapability(Vehicle.Capability.BIKE)) {
-					View.VISIBLE
-				} else {
-					View.GONE
-				}
-			findViewById<ImageView>(R.id.voice).visibility =
-				if (departure.vehicle.getCapability(Vehicle.Capability.VOICE)) {
-					View.VISIBLE
-				} else {
-					View.GONE
-				}
-			findViewById<ImageView>(R.id.ticket).visibility =
-				if (departure.vehicle.let {
+			findViewById<ImageView>(R.id.voice).let {
+				TooltipCompat.setTooltipText(
+					it,
+					getString(R.string.voice_announcements_content_description)
+				)
+				it.visibility =
+					if (departure.vehicle.getCapability(Vehicle.Capability.VOICE)) {
+						View.VISIBLE
+					} else {
+						View.GONE
+					}
+			}
+			findViewById<ImageView>(R.id.ticket).let { ticketImage ->
+				TooltipCompat.setTooltipText(
+					ticketImage,
+					getString(R.string.tickets_sold_content_description)
+				)
+				ticketImage.visibility = if (departure.vehicle.let {
 						it.getCapability(Vehicle.Capability.TICKET_DRIVER) || it.getCapability(Vehicle.Capability.TICKET_MACHINE)
 					}) {
 					View.VISIBLE
 				} else {
 					View.GONE
 				}
-			findViewById<ImageView>(R.id.usb).visibility =
-				if (departure.vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) {
-					View.VISIBLE
-				} else {
-					View.GONE
+			}
+			findViewById<ImageView>(R.id.usb).let {
+				TooltipCompat.setTooltipText(
+					it,
+					getString(R.string.usb_charging_content_description)
+				)
+				it.visibility =
+					if (departure.vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) {
+						View.VISIBLE
+					} else {
+						View.GONE
+					}
+			}
+
+			if (departure.alerts.isNotEmpty()) {
+				findViewById<MaterialTextView>(R.id.alerts).let { alertsView ->
+					alertsView.text = departure.alerts.joinToString(separator = "\n") { it.header }
+					alertsView.visibility = View.VISIBLE
 				}
+			}
+
 			findViewById<MapView>(R.id.map).let { map ->
 				if (departure.vehicle.Position.isZero()) {
 					map.visibility = View.GONE
@@ -259,13 +312,13 @@ 				map.controller.apply {
 					GeoPoint(
 						departure.vehicle.location().latitude,
 						departure.vehicle.location().longitude
-					).let {
+					).let { geoPoint ->
 						if (updating) {
 							animateTo(
-								it, 19.0f.toDouble(), 3 * 1000
+								geoPoint, 19.0f.toDouble(), 3 * 1000
 							)
 						} else {
-							setCenter(it)
+							setCenter(geoPoint)
 							setZoom(19f.toDouble())
 						}
 					}




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 4294211626b90a8acd68bf77db242f52020bd789..26bdd342b5a4b416f7f20748daf9387ff13c876a 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
@@ -13,6 +13,7 @@ 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.Time
 import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException
 import java.time.Instant
@@ -90,7 +91,8 @@ 	val time: Time,
 	val status: ULong,
 	val isRealtime: Boolean,
 	val vehicle: Vehicle,
-	val boarding: UByte
+	val boarding: UByte,
+	val alerts: List<Alert>
 ) {
 
 	constructor(d: DepartureV1) : this(
@@ -99,7 +101,8 @@ 		d.time,
 		d.status,
 		d.isRealtime,
 		Vehicle(d.vehicle),
-		d.boarding
+		d.boarding,
+		emptyList()
 	)
 
 	constructor(d: DepartureV2) : this(
@@ -108,7 +111,8 @@ 		d.time,
 		d.status,
 		d.isRealtime,
 		Vehicle(d.vehicle),
-		d.boarding
+		d.boarding,
+		emptyList()
 	)
 
 	constructor(d: DepartureV3) : this(
@@ -117,7 +121,18 @@ 		d.time,
 		d.status.ordinal.toULong(), // TODO VehicleStatus
 		d.isRealtime,
 		Vehicle(d.vehicle),
-		d.boarding
+		d.boarding,
+		emptyList()
+	)
+
+	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) }
 	)
 
 	fun statusText(context: Context?, showAsTime: Boolean, at: ZonedDateTime? = null): String {




diff --git a/app/src/main/res/layout/departure_bottom_sheet.xml b/app/src/main/res/layout/departure_bottom_sheet.xml
index 07fdfe3f032d1d913bd62072817aef68823c0c42..8efda21e81b9eff4bfe1c2587f52c793ec44e1b6 100644
--- a/app/src/main/res/layout/departure_bottom_sheet.xml
+++ b/app/src/main/res/layout/departure_bottom_sheet.xml
@@ -231,6 +231,15 @@ 		android:contentDescription="@string/usb_charging_content_description"
 		app:srcCompat="@drawable/usb"
 		tool:ignore="MissingConstraints" />
 
+	<com.google.android.material.textview.MaterialTextView
+		android:id="@+id/alerts"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_margin="16dp"
+		android:visibility="gone"
+		tool:text="Severe stops on Metropolitan line towards Tower Hill through Victoria"
+		app:layout_constraintTop_toBottomOf="@+id/capabilities" />
+
 	<org.osmdroid.views.MapView
 		android:id="@+id/map"
 		android:layout_width="match_parent"
@@ -238,5 +247,5 @@ 		android:layout_height="250dp"
 		android:layout_margin="16dp"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@+id/capabilities" />
+		app:layout_constraintTop_toBottomOf="@+id/alerts" />
 </androidx.constraintlayout.widget.ConstraintLayout>