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>