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>