Author: Adam <git@apiote.xyz>
add repository between application and api, and begin adding TRAFFIC v2
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt index b0ead8725723cdf1579ad1ff8cb9148ffeef32ab..3110ad3f67d1911ae1dd48e710cd3d5682e47de1 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Api.kt @@ -14,9 +14,7 @@ import java.net.URL import java.net.URLEncoder // todo [3.1] constants - -// todo [3.1] create Repository between models and api/fs -// todo [3.1] in Repository check if responses are BARE or HTML +// todo [3.1] split api files to classes files data class Server(val host: String, val token: String, val feeds: String, val apiPath: String) { companion object { diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Interfaces.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Interfaces.kt new file mode 100644 index 0000000000000000000000000000000000000000..17e5533d91f6f911a98be67bc0613f21e7d4541d --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Interfaces.kt @@ -0,0 +1,6 @@ +package xyz.apiote.bimba.czwek.api + +interface QueryableV1 +interface QueryableV2 +interface LocatableV1 +interface LocatableV2 \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Responses.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Responses.kt index e8dd16c9f3c4b61219bd49d49bee82ed08978bd4..2a0606d9c377a2fc44dc5b4ef879de6e96545c44 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Responses.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Responses.kt @@ -53,13 +53,13 @@ } data class DeparturesResponseDev( val alerts: List<AlertV1>, - val departures: List<DepartureV1>, - val stop: StopV1 + val departures: List<DepartureV2>, + val stop: StopV2 ) : DeparturesResponse { companion object { fun unmarshal(stream: InputStream): DeparturesResponseDev { val alerts = mutableListOf<AlertV1>() - val departures = mutableListOf<DepartureV1>() + val departures = mutableListOf<DepartureV2>() val reader = Reader(stream) val alertsNum = reader.readUInt().toULong() @@ -69,11 +69,11 @@ alerts.add(alert) } val departuresNum = reader.readUInt().toULong() for (i in 0UL until departuresNum) { - val departure = DepartureV1.unmarshal(stream) + val departure = DepartureV2.unmarshal(stream) departures.add(departure) } - return DeparturesResponseDev(alerts, departures, StopV1.unmarshal(stream)) + return DeparturesResponseDev(alerts, departures, StopV2.unmarshal(stream)) } } } @@ -108,9 +108,6 @@ when (val r = reader.readUInt().toULong()) { 0UL -> { queryables.add(StopV1.unmarshal(stream)) } - /*1UL -> { - queryables.add(Line.unmarshal(stream)) - }*/ else -> { throw UnknownResourceVersion("Queryable/$r", 1u) } @@ -121,20 +118,20 @@ } } } -data class QueryablesResponseDev(val queryables: List<QueryableV1>) : QueryablesResponse { +data class QueryablesResponseDev(val queryables: List<QueryableV2>) : QueryablesResponse { companion object { fun unmarshal(stream: InputStream): QueryablesResponseDev { - val queryables = mutableListOf<QueryableV1>() + val queryables = mutableListOf<QueryableV2>() val reader = Reader(stream) val n = reader.readUInt().toULong() for (i in 0UL until n) { when (val r = reader.readUInt().toULong()) { 0UL -> { - queryables.add(StopV1.unmarshal(stream)) + queryables.add(StopV2.unmarshal(stream)) + } + 1UL -> { + queryables.add(LineV1.unmarshal(stream)) } - /*1UL -> { - queryables.add(Line.unmarshal(stream)) - }*/ else -> { throw UnknownResourceVersion("Queryable/$r", 1u) } @@ -215,10 +212,10 @@ } } } -data class LocatablesResponseV1(val locatables: List<Locatable>) : LocatablesResponse { +data class LocatablesResponseV1(val locatables: List<LocatableV1>) : LocatablesResponse { companion object { fun unmarshal(stream: InputStream): LocatablesResponseV1 { - val locatables = mutableListOf<Locatable>() + val locatables = mutableListOf<LocatableV1>() val reader = Reader(stream) val n = reader.readUInt().toULong() for (i in 0UL until n) { @@ -238,19 +235,19 @@ return LocatablesResponseV1(locatables) } } } -data class LocatablesResponseDev(val locatables: List<Locatable>) : LocatablesResponse { +data class LocatablesResponseDev(val locatables: List<LocatableV2>) : LocatablesResponse { companion object { fun unmarshal(stream: InputStream): LocatablesResponseDev { - val locatables = mutableListOf<Locatable>() + val locatables = mutableListOf<LocatableV2>() val reader = Reader(stream) val n = reader.readUInt().toULong() for (i in 0UL until n) { when (val r = reader.readUInt().toULong()) { 0UL -> { - locatables.add(StopV1.unmarshal(stream)) + locatables.add(StopV2.unmarshal(stream)) } 1UL -> { - locatables.add(VehicleV1.unmarshal(stream)) + locatables.add(VehicleV2.unmarshal(stream)) } else -> { throw UnknownResourceVersion("Locatable/$r", 1u) 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 cf7adc236dc9653c04b5173789855dd53e10d317..b5bb6b933b8b419262ba34b9c615e5956206871d 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 @@ -1,33 +1,16 @@ package xyz.apiote.bimba.czwek.api -import android.content.Context import android.graphics.* -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable -import android.graphics.drawable.LayerDrawable import android.os.Parcelable -import android.text.format.DateUtils import android.util.Log -import androidx.appcompat.content.res.AppCompatResources -import androidx.core.graphics.ColorUtils.HSLToColor -import androidx.core.graphics.drawable.toBitmap import kotlinx.parcelize.Parcelize import org.yaml.snakeyaml.Yaml -import xyz.apiote.bimba.czwek.R -import xyz.apiote.bimba.czwek.dpToPixel -import xyz.apiote.bimba.czwek.dpToPixelI import xyz.apiote.fruchtfleisch.Reader import java.io.InputStream -import java.time.Instant -import java.time.ZoneId import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle -import java.time.temporal.ChronoUnit import java.util.* -import java.util.zip.Adler32 -import kotlin.math.abs -import kotlin.math.pow class TrafficFormatException(override val message: String) : IllegalArgumentException() class UnknownResourceVersion(val resource: String, val version: ULong) : Exception() @@ -108,9 +91,6 @@ @Parcelize data class PositionV1( val latitude: Double, val longitude: Double ) : Parcelable { - fun isZero(): Boolean { - return latitude == 0.0 && longitude == 0.0 - } override fun toString(): String = "$latitude,$longitude" @@ -198,14 +178,6 @@ reader.readU8(), reader.readU8(), reader.readU8() ) } } - - fun toInt(): Int { - var rgb = 0xff - rgb = (rgb shl 8) + R.toInt() - rgb = (rgb shl 8) + G.toInt() - rgb = (rgb shl 8) + B.toInt() - return rgb - } } data class VehicleV1( @@ -217,47 +189,7 @@ val Line: LineStubV1, val Headsign: String, val CongestionLevel: ULong, val OccupancyStatus: ULong -) : Locatable { - enum class Capability(val bit: UShort) { - RAMP(0b0001u), LOW_FLOOR(0b0010u), LOW_ENTRY(0b0001_0000_0000u), AC(0b0100u), BIKE(0b1000u), VOICE( - 0b0001_0000u - ), - TICKET_MACHINE(0b0010_0000u), TICKET_DRIVER(0b0100_0000u), USB_CHARGING(0b1000_0000u) - } - - override fun id(): String = ID - - override fun icon(context: Context, scale: Float): Drawable { - return BitmapDrawable(context.resources, Line.icon(context, scale)) - } - - override fun location(): PositionV1 = Position - - fun congestion(context: Context): String { - return when (val r = CongestionLevel.toUInt()) { // todo [3.1] enum - 0u -> context.getString(R.string.congestion_unknown) - 1u -> context.getString(R.string.congestion_smooth) - 2u -> context.getString(R.string.congestion_stop_and_go) - 3u -> context.getString(R.string.congestion_congestion) - 4u -> context.getString(R.string.congestion_jams) - else -> throw UnknownResourceVersion("Congestion/$r", 1u) - } - } - - fun occupancy(context: Context): String { - return when (val r = OccupancyStatus.toUInt()) { // todo [3.1] enum - 0u -> context.getString(R.string.occupancy_unknown) - 1u -> context.getString(R.string.occupancy_empty) - 2u -> context.getString(R.string.occupancy_many_seats) - 3u -> context.getString(R.string.occupancy_few_seats) - 4u -> context.getString(R.string.occupancy_standing_only) - 5u -> context.getString(R.string.occupancy_crowded) - 6u -> context.getString(R.string.occupancy_full) - 7u -> context.getString(R.string.occupancy_wont_let) - else -> throw UnknownResourceVersion("Occupancy/$r", 1u) - } - } - +):LocatableV1 { companion object { fun unmarshal(stream: InputStream): VehicleV1 { val reader = Reader(stream) @@ -273,15 +205,38 @@ reader.readUInt().toULong() ) } } +} - fun getCapability(field: Capability): Boolean { - return Capabilities.and(field.bit) != (0).toUShort() +data class VehicleV2( + val ID: String, + val Position: PositionV1, + val Capabilities: UShort, + val Speed: Float, + val Line: LineStubV2, + val Headsign: String, + val CongestionLevel: ULong, + val OccupancyStatus: ULong +): LocatableV2 { + companion object { + fun unmarshal(stream: InputStream): VehicleV2 { + val reader = Reader(stream) + return VehicleV2( + reader.readString(), + PositionV1.unmarshal(stream), + reader.readU16(), + reader.readFloat32(), + LineStubV2.unmarshal(stream), + reader.readString(), + reader.readUInt().toULong(), + reader.readUInt().toULong() + ) + } } } data class LineStubV1( val name: String, val kind: LineTypeV1, val colour: ColourV1 -) : LineAbstract { +) { companion object { fun unmarshal(stream: InputStream): LineStubV1 { val reader = Reader(stream) @@ -292,9 +247,20 @@ ColourV1.unmarshal(stream) ) } } +} - fun icon(context: Context, scale: Float = 1f): Bitmap { - return super.icon(context, kind, colour, scale) +data class LineStubV2( + val name: String, val kind: LineTypeV2, val colour: ColourV1 +) { + companion object { + fun unmarshal(stream: InputStream): LineStubV2 { + val reader = Reader(stream) + return LineStubV2( + reader.readString(), + LineTypeV2.of(reader.readUInt().toULong().toUInt()), + ColourV1.unmarshal(stream) + ) + } } } @@ -307,47 +273,6 @@ val vehicle: VehicleV1, val boarding: UByte ) { - fun statusText(context: Context?): String { - val now = 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) - return when (val r = status.toUInt()) { - 0u -> 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 UnknownResourceVersion("VehicleStatus/$r", 1u) - } - } - - fun timeString(context: Context): String { - return if (isRealtime) { - context.getString( - R.string.at_time_realtime, time.Hour.toInt(), time.Minute.toInt(), time.Second.toInt() - ) - } else { - context.getString(R.string.at_time, time.Hour.toInt(), time.Minute.toInt()) - } - } - - 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.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) - } - } - companion object { fun unmarshal(stream: InputStream): DepartureV1 { val reader = Reader(stream) @@ -362,84 +287,74 @@ } } } -interface QueryableV1 -interface Locatable { - fun icon(context: Context, scale: Float = 1f): Drawable - fun location(): PositionV1 - fun id(): String -} - -class ErrorLocatable(val stringResource: Int) : Locatable { - override fun icon(context: Context, scale: Float): Drawable { - return AppCompatResources.getDrawable(context, R.drawable.error_other)!! - } - - override fun location(): PositionV1 { - return PositionV1(0.0, 0.0) - } +data class DepartureV2( + val ID: String, + val time: Time, + val status: ULong, + val isRealtime: Boolean, + val vehicle: VehicleV2, + val boarding: UByte +) { - override fun id(): String { - return "ERROR" + companion object { + fun unmarshal(stream: InputStream): DepartureV2 { + val reader = Reader(stream) + val id = reader.readString() + val time = Time.unmarshal(stream) + val status = reader.readUInt().toULong() + val isRealtime = reader.readBoolean() + val vehicle = VehicleV2.unmarshal(stream) + val boarding = reader.readU8() + return DepartureV2(id, time, status, isRealtime, vehicle, boarding) + } } } @Parcelize -data class StopV1( +data class StopV2( val code: String, val name: String, + val nodeName: String, val zone: String, + val feedID: String, val position: PositionV1, val changeOptions: List<ChangeOptionV1> -) : QueryableV1, Locatable, Parcelable { - - override fun icon(context: Context, scale: Float): Drawable { - val saturationArray = arrayOf(0.5f, 0.65f, 0.8f) - val sal = saturationArray.size - val lightnessArray = arrayOf(.5f) - val lal = lightnessArray.size - val md = Adler32().let { - it.update(name.toByteArray()) - it.value - } - val h = md % 359f - val s = saturationArray[(md / 360 % sal).toInt()] - val l = lightnessArray[(md / 360 / sal % lal).toInt()] - val fg = AppCompatResources.getDrawable(context, R.drawable.stop) - val bg = AppCompatResources.getDrawable(context, R.drawable.stop_bg)!!.mutate().apply { - setTint(HSLToColor(arrayOf(h, s, l).toFloatArray())) +) : QueryableV2, Parcelable, LocatableV2 { + companion object { + fun unmarshal(stream: InputStream): StopV2 { + val reader = Reader(stream) + val code = reader.readString() + val name = reader.readString() + val nodeName = reader.readString() + val zone = reader.readString() + val feedID = reader.readString() + val position = PositionV1.unmarshal(stream) + val chOptionsNum = reader.readUInt().toULong() + val changeOptions = mutableListOf<ChangeOptionV1>() + for (i in 0UL until chOptionsNum) { + changeOptions.add(ChangeOptionV1.unmarshal(stream)) + } + return StopV2( + name = name, + nodeName = nodeName, + code = code, + zone = zone, + position = position, + feedID = feedID, + changeOptions = changeOptions + ) } - return BitmapDrawable( - context.resources, - LayerDrawable(arrayOf(bg, fg)).mutate() - .toBitmap(dpToPixelI(24f / scale), dpToPixelI(24f / scale), Bitmap.Config.ARGB_8888) - ) } - - override fun id(): String = code - - override fun location(): PositionV1 = position - - override fun toString(): String { - var result = "$name ($code) [$zone] $position\n" - for (chOpt in changeOptions) result += "${chOpt.line} ā ${chOpt.headsign}\n" - return result - } - - fun changeOptions(context: Context): Pair<String, String> { - return Pair(changeOptions.groupBy { it.line } - .map { Pair(it.key, it.value.joinToString { co -> co.headsign }) }.joinToString { - context.getString( - R.string.vehicle_headsign, it.first, it.second - ) - }, - changeOptions.groupBy { it.line } - .map { Pair(it.key, it.value.joinToString { co -> co.headsign }) }.joinToString { - context.getString( - R.string.vehicle_headsign_content_description, it.first, it.second - ) - }) - } +} +@Parcelize +data class StopV1( + val code: String, + val name: String, + val zone: String, + val position: PositionV1, + val changeOptions: List<ChangeOptionV1> +) : QueryableV1, Parcelable, LocatableV1 { companion object { fun unmarshal(stream: InputStream): StopV1 { val reader = Reader(stream) @@ -453,87 +368,17 @@ for (i in 0UL until chOptionsNum) { changeOptions.add(ChangeOptionV1.unmarshal(stream)) } return StopV1( - name = name, code = code, zone = zone, position = position, changeOptions = changeOptions + name = name, + code = code, + zone = zone, + position = position, + changeOptions = changeOptions ) } } } -interface LineAbstract { - fun textColour(c: ColourV1): Int { - val black = relativeLuminance(ColourV1(0u, 0u, 0u)) + .05 - val white = relativeLuminance(ColourV1(255u, 255u, 255u)) + .05 - val colour = relativeLuminance(c) + .05 - return if ((white / colour) > (colour / black)) { - Color.WHITE - } else { - Color.BLACK - } - } - - private fun relativeLuminance(colour: ColourV1): Double { - val r = fromSRGB(colour.R.toDouble() / 0xff) - val g = fromSRGB(colour.G.toDouble() / 0xff) - val b = fromSRGB(colour.B.toDouble() / 0xff) - return 0.2126 * r + 0.7152 * g + 0.0722 * b - } - - private fun fromSRGB(part: Double): Double { - return if (part <= 0.03928) { - part / 12.92 - } else { - ((part + 0.055) / 1.055).pow(2.4) - } - } - - fun icon(context: Context, type: LineTypeV1, colour: ColourV1, scale: Float): Bitmap { - val drawingBitmap = Bitmap.createBitmap( - dpToPixelI(24f / scale), dpToPixelI(24f / scale), Bitmap.Config.ARGB_8888 - ) - val canvas = Canvas(drawingBitmap) - - canvas.drawPath(getSquirclePath( - dpToPixel(.8f / scale), dpToPixel(.8f / scale), dpToPixelI(11.2f / scale) - ), Paint().apply { color = textColour(colour) }) - canvas.drawPath(getSquirclePath( - dpToPixel(1.6f / scale), dpToPixel(1.6f / scale), dpToPixelI(10.4f / scale) - ), Paint().apply { color = colour.toInt() }) - - val iconID = when (type) { - LineTypeV1.BUS -> R.drawable.bus_black - LineTypeV1.TRAM -> R.drawable.tram_black - LineTypeV1.UNKNOWN -> R.drawable.vehicle_black - } - val icon = AppCompatResources.getDrawable(context, iconID)?.mutate()?.apply { - setTint(textColour(colour)) - }?.toBitmap(dpToPixelI(19.2f / scale), dpToPixelI(19.2f / scale), Bitmap.Config.ARGB_8888) - canvas.drawBitmap( - icon!!, dpToPixel(2.4f / scale), dpToPixel(2.4f / scale), Paint() - ) - return drawingBitmap - } - - private fun getSquirclePath( - left: Float, top: Float, radius: Int - ): Path { - val radiusToPow = (radius * radius * radius).toDouble() - val path = Path() - path.moveTo(-radius.toFloat(), 0f) - for (x in -radius..radius) path.lineTo( - x.toFloat(), Math.cbrt(radiusToPow - abs(x * x * x)).toFloat() - ) - for (x in radius downTo -radius) path.lineTo( - x.toFloat(), -Math.cbrt(radiusToPow - abs(x * x * x)).toFloat() - ) - path.close() - val matrix = Matrix() - matrix.postTranslate((left + radius), (top + radius)) - path.transform(matrix) - return path - } -} - -data class Line( +data class LineV1( val colour: ColourV1, val type: LineTypeV1, val headsignsThere: List<String>, @@ -541,17 +386,13 @@ val headsignsBack: List, val graphThere: LineGraph, val graphBack: LineGraph, val name: String -) : QueryableV1, LineAbstract { +) : QueryableV2 { override fun toString(): String { return "$name ($type) [$colour]\nā [${headsignsThere.joinToString()}]\nā [${headsignsBack.joinToString()}]\n" } - fun icon(context: Context, scale: Float = 1f): Bitmap { - return super.icon(context, type, colour, scale) - } - companion object { - fun unmarshal(stream: InputStream): Line { + fun unmarshal(stream: InputStream): LineV1 { val reader = Reader(stream) val colour = ColourV1.unmarshal(stream) val type = reader.readUInt() @@ -568,7 +409,7 @@ } val graphThere = LineGraph.unmarshal(stream) val graphBack = LineGraph.unmarshal(stream) val name = reader.readString() - return Line( + return LineV1( name = name, colour = colour, type = LineTypeV1.of(type.toULong().toUInt()), @@ -595,9 +436,24 @@ } } } } +enum class LineTypeV2 { + UNKNOWN, TRAM, BUS, TROLLEYBUS; + + companion object { + fun of(type: UInt): LineTypeV2 { + return when (type) { + 0u -> valueOf("UNKNOWN") + 1u -> valueOf("TRAM") + 2u -> valueOf("BUS") + 3u -> valueOf("TROLLEYBUS") + else -> throw UnknownResourceVersion("LineType/$type", 1u) + } + } + } +} @Parcelize -data class ChangeOptionV1(val line: String, val headsign: String):Parcelable { +data class ChangeOptionV1(val line: String, val headsign: String) : Parcelable { companion object { fun unmarshal(stream: InputStream): ChangeOptionV1 { val reader = Reader(stream) diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt index a945d9e2fb2bb306325e15874d424a612701b808..e52278a89138a79ec2e541347f421c6af6000ea8 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt @@ -17,11 +17,11 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.search.SearchView -import xyz.apiote.bimba.czwek.api.Line -import xyz.apiote.bimba.czwek.api.StopV1 import xyz.apiote.bimba.czwek.dashboard.MainActivity import xyz.apiote.bimba.czwek.databinding.FragmentHomeBinding import xyz.apiote.bimba.czwek.departures.DeparturesActivity +import xyz.apiote.bimba.czwek.repo.Line +import xyz.apiote.bimba.czwek.repo.Stop import xyz.apiote.bimba.czwek.search.BimbaResultsAdapter class HomeFragment : Fragment() { @@ -73,7 +73,7 @@ } binding.suggestionsRecycler.layoutManager = LinearLayoutManager(activity) adapter = BimbaResultsAdapter(layoutInflater, activity, listOf()) { when (it) { - is StopV1 -> { + is Stop -> { val intent = Intent(activity, DeparturesActivity::class.java).apply { putExtra("code", it.code) putExtra("name", it.name) 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 08bac2980eae007f18460cb14f3bbf26b76a4320..9430921f4181c3cfabd1614980b6bd15ffdf269e 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 @@ -11,48 +11,30 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import xyz.apiote.bimba.czwek.api.ErrorResponse -import xyz.apiote.bimba.czwek.api.QueryableV1 -import xyz.apiote.bimba.czwek.api.QueryablesResponse -import xyz.apiote.bimba.czwek.api.QueryablesResponseDev -import xyz.apiote.bimba.czwek.api.QueryablesResponseV1 -import xyz.apiote.bimba.czwek.api.Server -import xyz.apiote.bimba.czwek.api.queryQueryables +import xyz.apiote.bimba.czwek.repo.ErrorResponseError +import xyz.apiote.bimba.czwek.repo.OnlineRepository +import xyz.apiote.bimba.czwek.repo.Queryable class HomeViewModel : ViewModel() { - private val mutableQueryables = MutableLiveData<List<QueryableV1>>() - val queryables: LiveData<List<QueryableV1>> = mutableQueryables + private val mutableQueryables = MutableLiveData<List<Queryable>>() + val queryables: LiveData<List<Queryable>> = mutableQueryables - fun getQueryables(cm: ConnectivityManager, server: Server, query: String) { + fun getQueryables(cm: ConnectivityManager, query: String, context: Context) { viewModelScope.launch { - val result = queryQueryables(cm, server, query, limit = 12) - if (result.error != null) { + try { + val repository = OnlineRepository() + mutableQueryables.value = repository.queryQueryables(cm, query, context) ?: emptyList() + } catch (e: ErrorResponseError) { // xxx intentionally no error showing in suggestions - if (result.stream != null) { - val response = withContext(Dispatchers.IO) {ErrorResponse.unmarshal(result.stream)} - Log.e("Suggestion", "${result.error.statusCode}, ${response.message}") - } else { - Log.e("Suggestion", "${result.error.statusCode}") - } - } else { - mutableQueryables.value = - when (val response = withContext(Dispatchers.IO) {QueryablesResponse.unmarshal(result.stream!!)}) { - is QueryablesResponseDev -> response.queryables - is QueryablesResponseV1 -> response.queryables - else -> null - } + Log.e("Suggestion", "$e") } } } inner class SearchBarWatcher( - private val context: Context, - private val cm: ConnectivityManager - ) : - TextWatcher { + private val context: Context, private val cm: ConnectivityManager + ) : TextWatcher { private val handler = Handler(Looper.getMainLooper()) private var workRunnable = Runnable {} @@ -66,14 +48,10 @@ override fun afterTextChanged(s: Editable?) { handler.removeCallbacks(workRunnable) workRunnable = Runnable { val text = s.toString() - getQueryables( - cm, - Server.get(context), text - ) + getQueryables(cm, text, context) } handler.postDelayed( - workRunnable, - 750 + workRunnable, 750 ) // todo(ux,low) make good time (probably between 500, 1000ms) } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt index 00b2d223e7309cbb6c1deb5d1fa1badfce1402d6..b3bab9bfc53bcb305783f44410b49de895f51adb 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapFragment.kt @@ -19,13 +19,6 @@ import androidx.core.graphics.drawable.toBitmap import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import com.google.android.material.snackbar.Snackbar -import xyz.apiote.bimba.czwek.R -import xyz.apiote.bimba.czwek.api.ErrorLocatable -import xyz.apiote.bimba.czwek.api.PositionV1 -import xyz.apiote.bimba.czwek.api.Server -import xyz.apiote.bimba.czwek.dashboard.MainActivity -import xyz.apiote.bimba.czwek.databinding.FragmentMapBinding -import xyz.apiote.bimba.czwek.dpToPixelI import org.osmdroid.config.Configuration import org.osmdroid.events.MapListener import org.osmdroid.events.ScrollEvent @@ -38,6 +31,12 @@ import org.osmdroid.views.overlay.TilesOverlay import org.osmdroid.views.overlay.gestures.RotationGestureOverlay import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay +import xyz.apiote.bimba.czwek.R +import xyz.apiote.bimba.czwek.dashboard.MainActivity +import xyz.apiote.bimba.czwek.databinding.FragmentMapBinding +import xyz.apiote.bimba.czwek.dpToPixelI +import xyz.apiote.bimba.czwek.repo.ErrorLocatable +import xyz.apiote.bimba.czwek.repo.Position class MapFragment : Fragment() { @@ -135,7 +134,8 @@ marker is Marker } if (it.size == 1 && it[0] is ErrorLocatable) { - Snackbar.make(binding.root, (it[0] as ErrorLocatable).stringResource, Snackbar.LENGTH_LONG).show() + Snackbar.make(binding.root, (it[0] as ErrorLocatable).stringResource, Snackbar.LENGTH_LONG) + .show() return@observe } @@ -181,16 +181,13 @@ private fun getLocatables() { maybeBinding?.let { binding -> val (bl, tr) = binding.map.boundingBox.let { Pair( - PositionV1(it.latSouth, it.lonWest), - PositionV1(it.latNorth, it.lonEast) + Position(it.latSouth, it.lonWest), + Position(it.latNorth, it.lonEast) ) } context?.let { val cm = it.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - mapViewModel.getLocatablesIn( - cm, - Server.get(it), bl, tr - ) + mapViewModel.getLocatablesIn(cm, bl, tr, it) } delayGetLocatables(30000) } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt index 1c3111a2e39e2dc4a0661829ae6c59847c3660e4..87e4c42fd45e211bd5c8a54c700a5cd63fea54fb 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/map/MapViewModel.kt @@ -1,6 +1,7 @@ package xyz.apiote.bimba.czwek.dashboard.ui.map import android.content.ActivityNotFoundException +import android.content.Context import android.content.Intent import android.net.ConnectivityManager import android.net.Uri @@ -18,46 +19,29 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import xyz.apiote.bimba.czwek.R -import xyz.apiote.bimba.czwek.api.ErrorLocatable -import xyz.apiote.bimba.czwek.api.ErrorResponse -import xyz.apiote.bimba.czwek.api.Locatable -import xyz.apiote.bimba.czwek.api.LocatablesResponse -import xyz.apiote.bimba.czwek.api.LocatablesResponseDev -import xyz.apiote.bimba.czwek.api.LocatablesResponseV1 -import xyz.apiote.bimba.czwek.api.PositionV1 -import xyz.apiote.bimba.czwek.api.Server -import xyz.apiote.bimba.czwek.api.StopV1 -import xyz.apiote.bimba.czwek.api.VehicleV1 import xyz.apiote.bimba.czwek.departures.DeparturesActivity +import xyz.apiote.bimba.czwek.repo.ErrorResponseError +import xyz.apiote.bimba.czwek.repo.Locatable +import xyz.apiote.bimba.czwek.repo.OnlineRepository +import xyz.apiote.bimba.czwek.repo.Position +import xyz.apiote.bimba.czwek.repo.Stop +import xyz.apiote.bimba.czwek.repo.Vehicle class MapViewModel : ViewModel() { private val _locatables = MutableLiveData<List<Locatable>>() val locatables: MutableLiveData<List<Locatable>> = _locatables - fun getLocatablesIn(cm: ConnectivityManager, server: Server, bl: PositionV1, tr: PositionV1) { + fun getLocatablesIn(cm: ConnectivityManager, bl: Position, tr: Position, context: Context) { viewModelScope.launch { - val result = xyz.apiote.bimba.czwek.api.getLocatablesIn(cm, server, bl, tr) - if (result.error != null) { - _locatables.value = listOf(ErrorLocatable(result.error.stringResource)) - if (result.stream != null) { - Log.w( - "Map", - "${result.error.statusCode}, ${withContext(Dispatchers.IO) {ErrorResponse.unmarshal(result.stream).message}}" - ) - } else { - Log.w("Map", "${result.error.statusCode}") - } - return@launch - } else { - _locatables.value = when (val response = withContext(Dispatchers.IO) {LocatablesResponse.unmarshal(result.stream!!)}) { - is LocatablesResponseDev -> response.locatables - is LocatablesResponseV1 -> response.locatables - else -> null + viewModelScope.launch { + try { + val repository = OnlineRepository() + _locatables.value = repository.getLocatablesIn(cm, bl, tr, context) ?: emptyList() + } catch (e: ErrorResponseError) { + Log.w("Map", "$e") } } } @@ -69,7 +53,7 @@ companion object { const val TAG = "MapBottomSheet" } - private fun showVehicle(content: View, vehicle: VehicleV1) { + private fun showVehicle(content: View, vehicle: Vehicle) { content.findViewById<Group>(R.id.stop_group).visibility = View.GONE content.findViewById<Group>(R.id.vehicle_group).visibility = View.VISIBLE @@ -88,33 +72,33 @@ ctx.getString(R.string.speed_in_km_per_h, vehicle.Speed * 3.6) content.findViewById<TextView>(R.id.congestion_text).text = vehicle.congestion(ctx) content.findViewById<TextView>(R.id.occupancy_text).text = vehicle.occupancy(ctx) content.findViewById<ImageView>(R.id.ac).visibility = - if (vehicle.getCapability(VehicleV1.Capability.AC)) { + if (vehicle.getCapability(Vehicle.Capability.AC)) { View.VISIBLE } else { View.GONE } content.findViewById<ImageView>(R.id.bike).visibility = - if (vehicle.getCapability(VehicleV1.Capability.BIKE)) { + if (vehicle.getCapability(Vehicle.Capability.BIKE)) { View.VISIBLE } else { View.GONE } content.findViewById<ImageView>(R.id.voice).visibility = - if (vehicle.getCapability(VehicleV1.Capability.VOICE)) { + if (vehicle.getCapability(Vehicle.Capability.VOICE)) { View.VISIBLE } else { View.GONE } content.findViewById<ImageView>(R.id.ticket).visibility = if (vehicle.let { - it.getCapability(VehicleV1.Capability.TICKET_DRIVER) || it.getCapability(VehicleV1.Capability.TICKET_MACHINE) + it.getCapability(Vehicle.Capability.TICKET_DRIVER) || it.getCapability(Vehicle.Capability.TICKET_MACHINE) }) { View.VISIBLE } else { View.GONE } content.findViewById<ImageView>(R.id.usb).visibility = - if (vehicle.getCapability(VehicleV1.Capability.USB_CHARGING)) { + if (vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) { View.VISIBLE } else { View.GONE @@ -122,12 +106,11 @@ } } } - private fun showStop(content: View, stop: StopV1) { + private fun showStop(content: View, stop: Stop) { context?.let { ctx -> content.findViewById<Group>(R.id.stop_group).visibility = View.VISIBLE content.findViewById<Group>(R.id.vehicle_group).visibility = View.GONE - content.findViewById<TextView>(R.id.title).text = - context?.getString(R.string.stop_title, stop.name, stop.code) + content.findViewById<TextView>(R.id.title).text = stop.name content.findViewById<Button>(R.id.departures_button).setOnClickListener { val intent = Intent(ctx, DeparturesActivity::class.java).apply { putExtra("code", stop.code) @@ -166,11 +149,11 @@ ): View { val content = inflater.inflate(R.layout.map_bottom_sheet, container, false) content.apply { when (locatable) { - is VehicleV1 -> { + is Vehicle -> { showVehicle(this, locatable) } - is StopV1 -> { + is Stop -> { showStop(this, locatable) } } 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 152bd5c132ed4bc760c74e25d8a3abcb50f2abbe..9861781222203de84d18b8c0519e00f593d320a9 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 @@ -13,10 +13,6 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import xyz.apiote.bimba.czwek.R -import xyz.apiote.bimba.czwek.api.DepartureV1 -import xyz.apiote.bimba.czwek.api.VehicleV1 -import xyz.apiote.bimba.czwek.dpToPixelI import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.util.GeoPoint import org.osmdroid.views.CustomZoomButtonsController @@ -24,6 +20,10 @@ import org.osmdroid.views.MapView import org.osmdroid.views.overlay.Marker 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.Departure +import xyz.apiote.bimba.czwek.repo.Vehicle import java.util.* @@ -36,20 +36,24 @@ val headsign: TextView = itemView.findViewById(R.id.departure_headsign) companion object { fun bind( - departure: DepartureV1, + departure: Departure, holder: BimbaDepartureViewHolder?, context: Context?, - onClickListener: (DepartureV1) -> Unit + onClickListener: (Departure) -> Unit ) { holder?.root?.setOnClickListener { onClickListener(departure) } - holder?.lineIcon?.setImageBitmap(departure.vehicle.Line.icon(context!!)) + holder?.lineIcon?.setImageDrawable(departure.vehicle.Line.icon(context!!)) holder?.lineIcon?.contentDescription = departure.vehicle.Line.kind.name holder?.lineName?.text = departure.vehicle.Line.name - holder?.headsign?.text = context?.getString(R.string.departure_headsign, departure.vehicle.Headsign) + holder?.headsign?.text = + context?.getString(R.string.departure_headsign, departure.vehicle.Headsign) holder?.headsign?.contentDescription = - context?.getString(R.string.departure_headsign_content_description, departure.vehicle.Headsign) + context?.getString( + R.string.departure_headsign_content_description, + departure.vehicle.Headsign + ) holder?.departureTime?.text = departure.statusText(context) } @@ -59,8 +63,8 @@ class BimbaDeparturesAdapter( private val inflater: LayoutInflater, private val context: Context?, - private var departures: List<DepartureV1>, - private val onClickListener: ((DepartureV1) -> Unit) + private var departures: List<Departure>, + private val onClickListener: ((Departure) -> Unit) ) : RecyclerView.Adapter<BimbaDepartureViewHolder>() { @@ -83,7 +87,7 @@ } override fun getItemCount(): Int = departures.size - fun get(ID: String): DepartureV1? { + fun get(ID: String): Departure? { val position = departuresPositions[ID] return if (position == null) { null @@ -93,7 +97,7 @@ } } @SuppressLint("NotifyDataSetChanged") // todo [3.1] DiffUtil - fun update(departures: List<DepartureV1>) { + fun update(departures: List<Departure>) { val newPositions: MutableMap<String, Int> = HashMap() departures.forEachIndexed { i, departure -> newPositions[departure.ID] = i @@ -105,7 +109,7 @@ notifyDataSetChanged() } } -class DepartureBottomSheet(private var departure: DepartureV1) : BottomSheetDialogFragment() { +class DepartureBottomSheet(private var departure: Departure) : BottomSheetDialogFragment() { companion object { const val TAG = "DepartureBottomSheet" } @@ -125,12 +129,12 @@ fun departureID(): String { return departure.ID } - fun update(departure: DepartureV1) { + fun update(departure: Departure) { this.departure = departure - this.view?.let { context?.let { ctx -> setContent(it, ctx) } } + this.view?.let { context?.let { ctx -> setContent(it, ctx, true) } } } - private fun setContent(view: View, ctx: Context) { + private fun setContent(view: View, ctx: Context, updating: Boolean = false) { view.apply { findViewById<TextView>(R.id.time).text = departure.timeString(ctx) @@ -143,8 +147,8 @@ } } findViewById<ImageView>(R.id.wheelchair_icon).apply { visibility = if (departure.vehicle.let { - it.getCapability(VehicleV1.Capability.LOW_FLOOR) || it.getCapability(VehicleV1.Capability.LOW_ENTRY) || it.getCapability( - VehicleV1.Capability.RAMP + it.getCapability(Vehicle.Capability.LOW_FLOOR) || it.getCapability(Vehicle.Capability.LOW_ENTRY) || it.getCapability( + Vehicle.Capability.RAMP ) }) { View.VISIBLE @@ -159,7 +163,11 @@ R.string.vehicle_headsign_content_description, departure.vehicle.Line.name, departure.vehicle.Headsign ) - text = getString(R.string.vehicle_headsign, departure.vehicle.Line.name, departure.vehicle.Headsign) + text = getString( + R.string.vehicle_headsign, + departure.vehicle.Line.name, + departure.vehicle.Headsign + ) } findViewById<TextView>(R.id.boarding_text).text = departure.boardingText(ctx) @@ -170,33 +178,33 @@ findViewById (R.id.congestion_text).text = departure.vehicle.congestion(ctx) findViewById<TextView>(R.id.occupancy_text).text = departure.vehicle.occupancy(ctx) findViewById<ImageView>(R.id.ac).visibility = - if (departure.vehicle.getCapability(VehicleV1.Capability.AC)) { + if (departure.vehicle.getCapability(Vehicle.Capability.AC)) { View.VISIBLE } else { View.GONE } findViewById<ImageView>(R.id.bike).visibility = - if (departure.vehicle.getCapability(VehicleV1.Capability.BIKE)) { + if (departure.vehicle.getCapability(Vehicle.Capability.BIKE)) { View.VISIBLE } else { View.GONE } findViewById<ImageView>(R.id.voice).visibility = - if (departure.vehicle.getCapability(VehicleV1.Capability.VOICE)) { + if (departure.vehicle.getCapability(Vehicle.Capability.VOICE)) { View.VISIBLE } else { View.GONE } findViewById<ImageView>(R.id.ticket).visibility = if (departure.vehicle.let { - it.getCapability(VehicleV1.Capability.TICKET_DRIVER) || it.getCapability(VehicleV1.Capability.TICKET_MACHINE) + 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(VehicleV1.Capability.USB_CHARGING)) { + if (departure.vehicle.getCapability(Vehicle.Capability.USB_CHARGING)) { View.VISIBLE } else { View.GONE @@ -227,7 +235,10 @@ marker is Marker } val marker = Marker(map).apply { position = - GeoPoint(departure.vehicle.location().latitude, departure.vehicle.location().longitude) + GeoPoint( + departure.vehicle.location().latitude, + departure.vehicle.location().longitude + ) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) icon = context?.let { ctx -> departure.vehicle.icon(ctx, 2f) } setOnClickListener {} 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 456c3dee749cc91e33105568ca1243c2946787fb..7d5de6075ae12ef87050206d3da39cf15077bae8 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 @@ -17,6 +17,10 @@ import kotlinx.coroutines.* import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.api.* import xyz.apiote.bimba.czwek.databinding.ActivityDeparturesBinding +import xyz.apiote.bimba.czwek.repo.Departure +import xyz.apiote.bimba.czwek.repo.ErrorResponseError +import xyz.apiote.bimba.czwek.repo.OnlineRepository +import xyz.apiote.bimba.czwek.repo.Stop class DeparturesActivity : AppCompatActivity() { private var _binding: ActivityDeparturesBinding? = null @@ -83,31 +87,15 @@ private fun getDepartures() { val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager MainScope().launch { - val result = getDepartures( - cm, - Server.get(this@DeparturesActivity), getCode() - ) - - if (result.error != null) { - showError(result.error) - if (result.stream != null) { - val response = withContext(Dispatchers.IO) {ErrorResponse.unmarshal(result.stream)} - Log.w("Departures", "${result.error.statusCode}, ${response.message}") - } else { - Log.w( - "Departures", - "${result.error.statusCode}, ${getString(result.error.stringResource)}" - ) - } - return@launch + try { + val repository = OnlineRepository() + val stopDepartures = repository.getDepartures(cm, getCode(), null, this@DeparturesActivity) + updateItems(stopDepartures!!.departures, stopDepartures.stop) + openBottomSheet?.departureID()?.let { adapter.get(it) }?.let { openBottomSheet?.update(it) } + } catch (e: ErrorResponseError) { + showError(e.error) + Log.w("Departures", "$e") } - val (departures, stop) = when (val response = withContext(Dispatchers.IO) {DeparturesResponse.unmarshal(result.stream!!)}) { - is DeparturesResponseDev -> Pair(response.departures, response.stop) - is DeparturesResponseV1 -> Pair(response.departures, response.stop) - else -> Pair(null, null) - } - updateItems(departures!!, stop!!) - openBottomSheet?.departureID()?.let { adapter.get(it) }?.let { openBottomSheet?.update(it) } } handler.removeCallbacks(runnable) runnable = Runnable { getDepartures() } @@ -124,7 +112,7 @@ binding.errorText.text = getString(error.stringResource) binding.errorImage.setImageDrawable(AppCompatResources.getDrawable(this, error.imageResource)) } - private fun updateItems(departures: List<DepartureV1>, stop: StopV1) { + private fun updateItems(departures: List<Departure>, stop: Stop) { binding.departuresProgress.visibility = View.GONE adapter.update(departures) binding.collapsingLayout.apply { diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/ChangeOption.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/ChangeOption.kt new file mode 100644 index 0000000000000000000000000000000000000000..9eafff01ab5ad63ea8140c49f0cb592c44f94d44 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/ChangeOption.kt @@ -0,0 +1,7 @@ +package xyz.apiote.bimba.czwek.repo + +import xyz.apiote.bimba.czwek.api.ChangeOptionV1 + +data class ChangeOption(val line: String, val headsign: String) { + constructor(c: ChangeOptionV1) : this(c.line, c.headsign) +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt new file mode 100644 index 0000000000000000000000000000000000000000..0c90453e769df3232fc0f92d00194446117f8b44 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt @@ -0,0 +1,15 @@ +package xyz.apiote.bimba.czwek.repo + +import xyz.apiote.bimba.czwek.api.ColourV1 + +data class Colour(val R: UByte, val G: UByte, val B: UByte) { + constructor(c: ColourV1) : this(c.R, c.G, c.B) + + fun toInt(): Int { + var rgb = 0xff + rgb = (rgb shl 8) + R.toInt() + rgb = (rgb shl 8) + G.toInt() + rgb = (rgb shl 8) + B.toInt() + return rgb + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt new file mode 100644 index 0000000000000000000000000000000000000000..d3259e6e5df6c5c5e0755f12ca47d48ede9e0c18 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt @@ -0,0 +1,100 @@ +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.AlertV1 +import xyz.apiote.bimba.czwek.api.DepartureV1 +import xyz.apiote.bimba.czwek.api.DepartureV2 +import xyz.apiote.bimba.czwek.api.Time +import xyz.apiote.bimba.czwek.api.UnknownResourceVersion +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit + +data class Alert( + val header: String, + val description: String, + val url: String, + val cause: ULong, // todo [3.1] enum + val effect: ULong // todo [3.1] enum +) { + constructor(a: AlertV1) : this(a.header, a.Description, a.Url, a.Cause, 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 +) { + + constructor(d: DepartureV1) : this( + d.ID, + d.time, + d.status, + d.isRealtime, + Vehicle(d.vehicle), + d.boarding + ) + + constructor(d: DepartureV2) : this( + d.ID, + d.time, + d.status, + d.isRealtime, + Vehicle(d.vehicle), + d.boarding + ) + + fun statusText(context: Context?): String { + val now = 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) + return when (val r = status.toUInt()) { + 0u -> 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 UnknownResourceVersion("VehicleStatus/$r", 1u) + } + } + + fun timeString(context: Context): String { + return if (isRealtime) { + context.getString( + R.string.at_time_realtime, time.Hour.toInt(), time.Minute.toInt(), time.Second.toInt() + ) + } else { + context.getString(R.string.at_time, time.Hour.toInt(), time.Minute.toInt()) + } + } + + 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.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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/ErrorLocatable.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/ErrorLocatable.kt new file mode 100644 index 0000000000000000000000000000000000000000..1d9562a1920bd04a00f04d37841c7c08607672f4 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/ErrorLocatable.kt @@ -0,0 +1,20 @@ +package xyz.apiote.bimba.czwek.repo + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.appcompat.content.res.AppCompatResources +import xyz.apiote.bimba.czwek.R + +class ErrorLocatable(val stringResource: Int) : Locatable { + override fun icon(context: Context, scale: Float): Drawable { + return AppCompatResources.getDrawable(context, R.drawable.error_other)!! + } + + override fun location(): Position { + return Position(0.0, 0.0) + } + + override fun id(): String { + return "ERROR" + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/ErrorResponseError.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/ErrorResponseError.kt new file mode 100644 index 0000000000000000000000000000000000000000..2afa66348ce58ae93a9863b6cb2449df1468059d --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/ErrorResponseError.kt @@ -0,0 +1,12 @@ +package xyz.apiote.bimba.czwek.repo + +import xyz.apiote.bimba.czwek.api.Error + +// todo that's a terrible name + +class ErrorResponseError(private val code: Int, private val msg: String, val error: Error) : + Exception() { + override fun toString(): String { + return "Error response with code $code: $msg" + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..e9045957685fd267c4d97a4cf2aff334adaf5da4 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt @@ -0,0 +1,40 @@ +package xyz.apiote.bimba.czwek.repo + +import android.content.Context +import android.graphics.drawable.Drawable +import android.net.ConnectivityManager + +interface Queryable +interface Locatable { + fun icon(context: Context, scale: Float = 1f): Drawable + fun location(): Position + fun id(): String +} + +interface Repository { + suspend fun getDepartures( + cm: ConnectivityManager, + stop: String, + line: String?, + context: Context + ): StopDepartures? + + suspend fun getLocatablesIn( + cm: ConnectivityManager, + bl: Position, + tr: Position, + context: Context + ): List<Locatable>? + + suspend fun queryQueryables( + cm: ConnectivityManager, + query: String, + context: Context + ): List<Queryable>? + + suspend fun locateQueryables( + cm: ConnectivityManager, + position: Position, + context: Context + ): List<Queryable>? +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Line.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Line.kt new file mode 100644 index 0000000000000000000000000000000000000000..a65bab7e10741eab719825d59f6d072697641c29 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Line.kt @@ -0,0 +1,19 @@ +package xyz.apiote.bimba.czwek.repo + +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable + +data class Line( + val colour: Colour, + val type: LineType, + val headsignsThere: List<String>, + val headsignsBack: List<String>, + val graphThere: LineGraph, + val graphBack: LineGraph, + val name: String +) : Queryable, LineAbstract { + fun icon(context: Context, scale: Float = 1f): Drawable { + return BitmapDrawable(context.resources, super.icon(context, type, colour, scale)) + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineAbstract.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineAbstract.kt new file mode 100644 index 0000000000000000000000000000000000000000..d3a0778b3595e3a215a478a0c7b77b0196582570 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineAbstract.kt @@ -0,0 +1,91 @@ +package xyz.apiote.bimba.czwek.repo + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.toBitmap +import xyz.apiote.bimba.czwek.R +import xyz.apiote.bimba.czwek.dpToPixel +import xyz.apiote.bimba.czwek.dpToPixelI +import kotlin.math.abs +import kotlin.math.pow + +interface LineAbstract { + fun textColour(c: Colour): Int { + val black = relativeLuminance(Colour(0u, 0u, 0u)) + .05 + val white = relativeLuminance(Colour(255u, 255u, 255u)) + .05 + val colour = relativeLuminance(c) + .05 + return if ((white / colour) > (colour / black)) { + Color.WHITE + } else { + Color.BLACK + } + } + + private fun relativeLuminance(colour: Colour): Double { + val r = fromSRGB(colour.R.toDouble() / 0xff) + val g = fromSRGB(colour.G.toDouble() / 0xff) + val b = fromSRGB(colour.B.toDouble() / 0xff) + return 0.2126 * r + 0.7152 * g + 0.0722 * b + } + + private fun fromSRGB(part: Double): Double { + return if (part <= 0.03928) { + part / 12.92 + } else { + ((part + 0.055) / 1.055).pow(2.4) + } + } + + fun icon(context: Context, type: LineType, colour: Colour, scale: Float): Bitmap { + val drawingBitmap = Bitmap.createBitmap( + dpToPixelI(24f / scale), dpToPixelI(24f / scale), Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(drawingBitmap) + + canvas.drawPath(getSquirclePath( + dpToPixel(.8f / scale), dpToPixel(.8f / scale), dpToPixelI(11.2f / scale) + ), Paint().apply { color = textColour(colour) }) + canvas.drawPath(getSquirclePath( + dpToPixel(1.6f / scale), dpToPixel(1.6f / scale), dpToPixelI(10.4f / scale) + ), Paint().apply { color = colour.toInt() }) + + val iconID = when (type) { + LineType.BUS -> R.drawable.bus_black + LineType.TRAM -> R.drawable.tram_black + LineType.TROLLEYBUS -> R.drawable.trolleybus_black + LineType.UNKNOWN -> R.drawable.vehicle_black + } + val icon = AppCompatResources.getDrawable(context, iconID)?.mutate()?.apply { + setTint(textColour(colour)) + }?.toBitmap(dpToPixelI(19.2f / scale), dpToPixelI(19.2f / scale), Bitmap.Config.ARGB_8888) + canvas.drawBitmap( + icon!!, dpToPixel(2.4f / scale), dpToPixel(2.4f / scale), Paint() + ) + return drawingBitmap + } + + private fun getSquirclePath( + left: Float, top: Float, radius: Int + ): Path { + val radiusToPow = (radius * radius * radius).toDouble() + val path = Path() + path.moveTo(-radius.toFloat(), 0f) + for (x in -radius..radius) path.lineTo( + x.toFloat(), Math.cbrt(radiusToPow - abs(x * x * x)).toFloat() + ) + for (x in radius downTo -radius) path.lineTo( + x.toFloat(), -Math.cbrt(radiusToPow - abs(x * x * x)).toFloat() + ) + path.close() + val matrix = Matrix() + matrix.postTranslate((left + radius), (top + radius)) + path.transform(matrix) + return path + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineGraph.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineGraph.kt new file mode 100644 index 0000000000000000000000000000000000000000..be4936a4ff66f16c48596e949c4f66cfb3c0192b --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineGraph.kt @@ -0,0 +1,7 @@ +package xyz.apiote.bimba.czwek.repo + +data class LineGraph( + val stops: List<StopStub>, + val nextNodes: Map<Long, List<Long>>, + val prevNodes: Map<Long, List<Long>> +) \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineStub.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineStub.kt new file mode 100644 index 0000000000000000000000000000000000000000..6f9f69264310cf126b42985bf1ab8557c4ddf95b --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineStub.kt @@ -0,0 +1,16 @@ +package xyz.apiote.bimba.czwek.repo + +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import xyz.apiote.bimba.czwek.api.LineStubV1 +import xyz.apiote.bimba.czwek.api.LineStubV2 + +data class LineStub(val name: String, val kind: LineType, val colour: Colour) : LineAbstract { + constructor(l: LineStubV1) : this(l.name, LineType.of(l.kind), Colour(l.colour)) + constructor(l: LineStubV2) : this(l.name, LineType.of(l.kind), Colour(l.colour)) + + fun icon(context: Context, scale: Float = 1f): Drawable { + return BitmapDrawable(context.resources, super.icon(context, kind, colour, scale)) + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineType.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineType.kt new file mode 100644 index 0000000000000000000000000000000000000000..ce3c5ec6b35bdd93a154cd2f1d99b8cfaf526b19 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineType.kt @@ -0,0 +1,26 @@ +package xyz.apiote.bimba.czwek.repo + +import xyz.apiote.bimba.czwek.api.LineTypeV1 +import xyz.apiote.bimba.czwek.api.LineTypeV2 + +enum class LineType { + UNKNOWN, TRAM, BUS, TROLLEYBUS; + + companion object { + fun of(t: LineTypeV1): LineType { + return when (t) { + LineTypeV1.UNKNOWN -> valueOf("UNKNOWN") + LineTypeV1.TRAM -> valueOf("TRAM") + LineTypeV1.BUS -> valueOf("BUS") + } + } + fun of(t: LineTypeV2): LineType { + return when (t) { + LineTypeV2.UNKNOWN -> valueOf("UNKNOWN") + LineTypeV2.TRAM -> valueOf("TRAM") + LineTypeV2.BUS -> valueOf("BUS") + LineTypeV2.TROLLEYBUS -> valueOf("TROLLEYBUS") + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..2a5eaa9f37a4b8d12e22b34305f0b4bebb7450b8 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt @@ -0,0 +1,150 @@ +package xyz.apiote.bimba.czwek.repo + +import android.content.Context +import android.net.ConnectivityManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import xyz.apiote.bimba.czwek.api.DeparturesResponse +import xyz.apiote.bimba.czwek.api.DeparturesResponseDev +import xyz.apiote.bimba.czwek.api.DeparturesResponseV1 +import xyz.apiote.bimba.czwek.api.ErrorResponse +import xyz.apiote.bimba.czwek.api.LocatablesResponse +import xyz.apiote.bimba.czwek.api.LocatablesResponseDev +import xyz.apiote.bimba.czwek.api.LocatablesResponseV1 +import xyz.apiote.bimba.czwek.api.PositionV1 +import xyz.apiote.bimba.czwek.api.QueryablesResponse +import xyz.apiote.bimba.czwek.api.QueryablesResponseDev +import xyz.apiote.bimba.czwek.api.QueryablesResponseV1 +import xyz.apiote.bimba.czwek.api.Server +import xyz.apiote.bimba.czwek.api.StopV1 +import xyz.apiote.bimba.czwek.api.StopV2 +import xyz.apiote.bimba.czwek.api.VehicleV1 +import xyz.apiote.bimba.czwek.api.VehicleV2 + +// todo [3.1] in Repository check if responses are BARE or HTML + +// todo add feedID +class OnlineRepository : Repository { + override suspend fun getDepartures( + cm: ConnectivityManager, + stop: String, + line: String?, + context: Context + ): StopDepartures? { + val result = xyz.apiote.bimba.czwek.api.getDepartures(cm, Server.get(context), stop, line) + if (result.error != null) { + if (result.stream != null) { + val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) } + throw ErrorResponseError(result.error.statusCode, response.message, result.error) + } else { + throw ErrorResponseError(result.error.statusCode, "", result.error) + } + } else { + return when (val response = + withContext(Dispatchers.IO) { DeparturesResponse.unmarshal(result.stream!!) }) { + is DeparturesResponseDev -> StopDepartures(response.departures.map { Departure(it) }, Stop(response.stop), response.alerts.map{Alert(it)}) + is DeparturesResponseV1 -> StopDepartures(response.departures.map { Departure(it) }, Stop(response.stop), response.alerts.map{Alert(it)}) + else -> null + } + } + } + + override suspend fun getLocatablesIn( + cm: ConnectivityManager, + bl: Position, + tr: Position, + context: Context + ): List<Locatable>? { + val result = xyz.apiote.bimba.czwek.api.getLocatablesIn( + cm, + Server.get(context), + PositionV1(bl.latitude, bl.longitude), + PositionV1(tr.latitude, tr.longitude) + ) + if (result.error != null) { + if (result.stream != null) { + val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) } + throw ErrorResponseError(result.error.statusCode, response.message, result.error) + } else { + throw ErrorResponseError(result.error.statusCode, "", result.error) + } + } else { + return when (val response = + withContext(Dispatchers.IO) { LocatablesResponse.unmarshal(result.stream!!) }) { + is LocatablesResponseDev -> response.locatables.map { + when (it) { + is StopV2 -> Stop(it) + is VehicleV2 -> Vehicle(it) + else -> TODO("nothing else") + } + } + + is LocatablesResponseV1 -> response.locatables.map { + when (it) { + is StopV1 -> Stop(it) + is VehicleV1 -> Vehicle(it) + else -> TODO("nothing else") + } + } + + else -> null + } + } + } + + override suspend fun queryQueryables( + cm: ConnectivityManager, query: String, context: Context + ): List<Queryable>? { + return getQueryables(cm, query, null, context, "query") + } + + override suspend fun locateQueryables( + cm: ConnectivityManager, position: Position, context: Context + ): List<Queryable>? { + return getQueryables(cm, null, position, context, "locate") + } + + private suspend fun getQueryables( + cm: ConnectivityManager, query: String?, position: Position?, context: Context, type: String + ): List<Queryable>? { + val result = when (type) { + "query" -> { + xyz.apiote.bimba.czwek.api.queryQueryables(cm, Server.get(context), query!!, limit = 12) + } + + "locate" -> xyz.apiote.bimba.czwek.api.locateQueryables( + cm, Server.get(context), PositionV1(position!!.latitude, position.longitude) + ) + + else -> TODO("Throw") + } + if (result.error != null) { + if (result.stream != null) { + val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) } + throw ErrorResponseError(result.error.statusCode, response.message, result.error) + } else { + throw ErrorResponseError(result.error.statusCode, "", result.error) + } + } else { + return when (val response = + withContext(Dispatchers.IO) { QueryablesResponse.unmarshal(result.stream!!) }) { + is QueryablesResponseDev -> response.queryables.map { + when (it) { + is StopV2 -> Stop(it) + // todo[lines] is LineV1 -> Line(it) + else -> TODO("nothing else") + } + } + + is QueryablesResponseV1 -> response.queryables.map { + when (it) { + is StopV1 -> Stop(it) + else -> TODO("nothing else") + } + } + + else -> null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Position.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Position.kt new file mode 100644 index 0000000000000000000000000000000000000000..14e0be5439f1beb32b98f7dbe146a507f59ff9f4 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Position.kt @@ -0,0 +1,10 @@ +package xyz.apiote.bimba.czwek.repo + +import xyz.apiote.bimba.czwek.api.PositionV1 + +data class Position(val latitude: Double, val longitude: Double) { + constructor(p: PositionV1) : this(p.latitude, p.longitude) + fun isZero(): Boolean { + return latitude == 0.0 && longitude == 0.0 + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt new file mode 100644 index 0000000000000000000000000000000000000000..6a90fadf4b806c2e5e5806ef1d6e343910030cf2 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt @@ -0,0 +1,90 @@ +package xyz.apiote.bimba.czwek.repo + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.drawable.toBitmap +import xyz.apiote.bimba.czwek.R +import xyz.apiote.bimba.czwek.api.StopV1 +import xyz.apiote.bimba.czwek.api.StopV2 +import xyz.apiote.bimba.czwek.dpToPixelI +import java.util.zip.Adler32 + +data class Stop( + val code: String, + val name: String, + val nodeName: String, + val zone: String, + val feedID: String?, + val position: Position, + val changeOptions: List<ChangeOption> +) : Queryable, Locatable { + + override fun icon(context: Context, scale: Float): Drawable { + val saturationArray = arrayOf(0.5f, 0.65f, 0.8f) + val sal = saturationArray.size + val lightnessArray = arrayOf(.5f) + val lal = lightnessArray.size + val md = Adler32().let { + it.update(nodeName.toByteArray()) + it.value + } + val h = md % 359f + val s = saturationArray[(md / 360 % sal).toInt()] + val l = lightnessArray[(md / 360 / sal % lal).toInt()] + val fg = AppCompatResources.getDrawable(context, R.drawable.stop) + val bg = AppCompatResources.getDrawable(context, R.drawable.stop_bg)!!.mutate().apply { + setTint(ColorUtils.HSLToColor(arrayOf(h, s, l).toFloatArray())) + } + return BitmapDrawable( + context.resources, + LayerDrawable(arrayOf(bg, fg)).mutate() + .toBitmap(dpToPixelI(24f / scale), dpToPixelI(24f / scale), Bitmap.Config.ARGB_8888) + ) + } + + override fun id(): String = code + + override fun location(): Position = position + + constructor(s: StopV1) : this( + s.code, + s.name, + s.name, + s.zone, + null, + Position(s.position), + s.changeOptions.map { ChangeOption(it) }) + + constructor(s: StopV2) : this( + s.code, + s.name, + s.nodeName, + s.zone, + s.feedID, + Position(s.position), + s.changeOptions.map { ChangeOption(it) }) + + fun changeOptions(context: Context): Pair<String, String> = Pair(changeOptions.groupBy { it.line } + .map { Pair(it.key, it.value.joinToString { co -> co.headsign }) }.joinToString { + context.getString( + R.string.vehicle_headsign, it.first, it.second + ) + }, + changeOptions.groupBy { it.line } + .map { Pair(it.key, it.value.joinToString { co -> co.headsign }) }.joinToString { + context.getString( + R.string.vehicle_headsign_content_description, it.first, it.second + ) + }) + + override fun toString(): String { + var result = "$name ($code) [$zone] $position\n" + for (chOpt in changeOptions) result += "${chOpt.line} ā ${chOpt.headsign}\n" + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/StopStub.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/StopStub.kt new file mode 100644 index 0000000000000000000000000000000000000000..58ca2abf7862876cb83f360d0fc4d4e33c4c863a --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/StopStub.kt @@ -0,0 +1,3 @@ +package xyz.apiote.bimba.czwek.repo + +data class StopStub(val name: String, val code: String, val zone: String, val onDemand: Boolean) \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Vehicle.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Vehicle.kt new file mode 100644 index 0000000000000000000000000000000000000000..4e7e795de8c92c2fdc69f1d3b01bbd391433ae37 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Vehicle.kt @@ -0,0 +1,68 @@ +package xyz.apiote.bimba.czwek.repo + +import android.content.Context +import android.graphics.drawable.Drawable +import xyz.apiote.bimba.czwek.R +import xyz.apiote.bimba.czwek.api.UnknownResourceVersion +import xyz.apiote.bimba.czwek.api.VehicleV1 +import xyz.apiote.bimba.czwek.api.VehicleV2 + +data class Vehicle( + val ID: String, + val Position: Position, + val Capabilities: UShort, + val Speed: Float, + val Line: LineStub, + val Headsign: String, + val CongestionLevel: ULong, + val OccupancyStatus: ULong +) : Locatable { + constructor(v: VehicleV1):this(v.ID, + Position(v.Position), v.Capabilities, v.Speed, + LineStub(v.Line), v.Headsign, v.CongestionLevel, v.OccupancyStatus) + constructor(v: VehicleV2):this(v.ID, + Position(v.Position), v.Capabilities, v.Speed, + LineStub(v.Line), v.Headsign, v.CongestionLevel, v.OccupancyStatus) + enum class Capability(val bit: UShort) { + RAMP(0b0001u), LOW_FLOOR(0b0010u), LOW_ENTRY(0b0001_0000_0000u), AC(0b0100u), BIKE(0b1000u), VOICE( + 0b0001_0000u + ), + TICKET_MACHINE(0b0010_0000u), TICKET_DRIVER(0b0100_0000u), USB_CHARGING(0b1000_0000u) + } + + override fun icon(context: Context, scale: Float): Drawable = Line.icon(context, scale) + + override fun location(): Position = Position + + override fun id(): String = ID + + fun congestion(context: Context): String { + return when (val r = CongestionLevel.toUInt()) { // todo [3.1] enum + 0u -> context.getString(R.string.congestion_unknown) + 1u -> context.getString(R.string.congestion_smooth) + 2u -> context.getString(R.string.congestion_stop_and_go) + 3u -> context.getString(R.string.congestion_congestion) + 4u -> context.getString(R.string.congestion_jams) + else -> throw UnknownResourceVersion("Congestion/$r", 1u) + } + } + + fun occupancy(context: Context): String { + return when (val r = OccupancyStatus.toUInt()) { // todo [3.1] enum + 0u -> context.getString(R.string.occupancy_unknown) + 1u -> context.getString(R.string.occupancy_empty) + 2u -> context.getString(R.string.occupancy_many_seats) + 3u -> context.getString(R.string.occupancy_few_seats) + 4u -> context.getString(R.string.occupancy_standing_only) + 5u -> context.getString(R.string.occupancy_crowded) + 6u -> context.getString(R.string.occupancy_full) + 7u -> context.getString(R.string.occupancy_wont_let) + else -> throw UnknownResourceVersion("Occupancy/$r", 1u) + + } + } + + fun getCapability(field: Capability): Boolean { + return Capabilities.and(field.bit) != (0).toUShort() + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt index 409962c789481b83f7f2530fe71e0a79d7df0ed4..a9790eb7dcecc35710c5bf0b8fccc1f1d6c9c486 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt @@ -9,9 +9,9 @@ import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import xyz.apiote.bimba.czwek.R -import xyz.apiote.bimba.czwek.api.Line -import xyz.apiote.bimba.czwek.api.QueryableV1 -import xyz.apiote.bimba.czwek.api.StopV1 +import xyz.apiote.bimba.czwek.repo.Line +import xyz.apiote.bimba.czwek.repo.Queryable +import xyz.apiote.bimba.czwek.repo.Stop class BimbaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View = itemView.findViewById(R.id.suggestion) @@ -21,13 +21,13 @@ val description: TextView = itemView.findViewById(R.id.suggestion_description) companion object { fun bind( - queryable: QueryableV1, + queryable: Queryable, holder: BimbaViewHolder?, context: Context?, - onClickListener: (QueryableV1) -> Unit + onClickListener: (Queryable) -> Unit ) { when (queryable) { - is StopV1 -> bindStop(queryable, holder, context) + is Stop -> bindStop(queryable, holder, context) //is Line -> bindLine(queryable, holder, context) } holder?.root?.setOnClickListener { @@ -35,12 +35,12 @@ onClickListener(queryable) } } - private fun bindStop(stop: StopV1, holder: BimbaViewHolder?, context: Context?) { + private fun bindStop(stop: Stop, holder: BimbaViewHolder?, context: Context?) { holder?.icon?.apply { setImageDrawable(stop.icon(context!!)) contentDescription = context.getString(R.string.stop_content_description) } - holder?.title?.text = context?.getString(R.string.stop_title, stop.name, stop.code) + holder?.title?.text = stop.name context?.let { stop.changeOptions(it).let { changeOptions -> holder?.description?.apply { @@ -53,7 +53,7 @@ } private fun bindLine(line: Line, holder: BimbaViewHolder?, context: Context?) { holder?.icon?.apply { - setImageBitmap(line.icon(context!!)) + setImageDrawable(line.icon(context!!)) contentDescription = line.type.name colorFilter = null } @@ -70,46 +70,28 @@ } } } -interface Adapter { - fun createViewHolder( - inflater: LayoutInflater, - layout: Int, - parent: ViewGroup - ): BimbaViewHolder { - val rowView = inflater.inflate(layout, parent, false) - return BimbaViewHolder(rowView) - } - - fun bindSuggestionHolder( - queryable: QueryableV1, - holder: BimbaViewHolder?, - context: Context?, - onClickListener: (QueryableV1) -> Unit - ) { - BimbaViewHolder.bind(queryable, holder, context, onClickListener) - } -} class BimbaResultsAdapter( private val inflater: LayoutInflater, private val context: Context?, - private var queryables: List<QueryableV1>, - private val onClickListener: ((QueryableV1) -> Unit) + private var queryables: List<Queryable>, + private val onClickListener: ((Queryable) -> Unit) ) : - RecyclerView.Adapter<BimbaViewHolder>(), Adapter { + RecyclerView.Adapter<BimbaViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaViewHolder { - return createViewHolder(inflater, R.layout.result, parent) + val rowView = inflater.inflate(R.layout.result, parent, false) + return BimbaViewHolder(rowView) } override fun onBindViewHolder(holder: BimbaViewHolder, position: Int) { - bindSuggestionHolder(queryables[position], holder, context, onClickListener) + BimbaViewHolder.bind(queryables[position], holder, context, onClickListener) } override fun getItemCount(): Int = queryables.size @SuppressLint("NotifyDataSetChanged") // todo [3.1] DiffUtil - fun update(queryables: List<QueryableV1>) { - this.queryables = queryables + fun update(queryables: List<Queryable>?) { + this.queryables = queryables ?: emptyList() notifyDataSetChanged() } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt index 72e786a2802d63564f3cf16d34fe1a6279313832..8e05512b78625496aba53970f9753d91914412d9 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt @@ -20,7 +20,12 @@ import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.api.* import xyz.apiote.bimba.czwek.databinding.ActivityResultsBinding import xyz.apiote.bimba.czwek.departures.DeparturesActivity -import java.io.InputStream +import xyz.apiote.bimba.czwek.repo.ErrorResponseError +import xyz.apiote.bimba.czwek.repo.Line +import xyz.apiote.bimba.czwek.repo.OnlineRepository +import xyz.apiote.bimba.czwek.repo.Position +import xyz.apiote.bimba.czwek.repo.Queryable +import xyz.apiote.bimba.czwek.repo.Stop class ResultsActivity : AppCompatActivity(), LocationListener { enum class Mode { @@ -43,7 +48,7 @@ binding.resultsRecycler.layoutManager = LinearLayoutManager(this) adapter = BimbaResultsAdapter(layoutInflater, this, listOf()) { when (it) { - is StopV1 -> { + is Stop -> { val intent = Intent(this, DeparturesActivity::class.java).apply { putExtra("code", it.code) putExtra("name", it.name) @@ -71,12 +76,12 @@ val query = intent.extras?.getString("query") val lat = intent.extras?.getDouble("lat") val lon = intent.extras?.getDouble("lon") supportActionBar?.title = getString(R.string.stops_near_code, query) - getQueryablesByLocation(Server.get(this), PositionV1(lat!!, lon!!)) + getQueryablesByLocation(Position(lat!!, lon!!), this) } Mode.MODE_SEARCH -> { val query = intent.extras?.getString("query")!! supportActionBar?.title = getString(R.string.results_for, query) - getQueryablesByQuery(Server.get(this), query) + getQueryablesByQuery(query, this) } } } @@ -101,7 +106,7 @@ } override fun onLocationChanged(location: Location) { handler.removeCallbacks(runnable) - getQueryablesByLocation(Server.get(this), PositionV1(location.latitude, location.longitude)) + getQueryablesByLocation(Position(location.latitude, location.longitude), this) } override fun onResume() { @@ -126,36 +131,30 @@ locationManager.removeUpdates(this) handler.removeCallbacks(runnable) } - private fun getQueryablesByQuery(server: Server, query: String) { + private fun getQueryablesByQuery(query: String, context: Context) { val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager MainScope().launch { - val result = queryQueryables(cm, server, query) - if (result.error != null) { - if (result.stream != null) { - val response = withContext(Dispatchers.IO) {ErrorResponse.unmarshal(result.stream)} - Log.w("Results", "${result.error.statusCode}, ${response.message}") - } else { - Log.w( - "Results", - "${result.error.statusCode}, ${getString(result.error.stringResource)}" - ) - } - showError(result.error) - } else { - updateItems(unmarshallQueryablesResponse(result.stream!!)!!) + try { + val repository = OnlineRepository() + val result = repository.queryQueryables(cm, query, context) + updateItems(result) + } catch (e: ErrorResponseError) { + Log.w("Suggestion", "$e") + showError(e.error) } } } - private fun getQueryablesByLocation(server: Server, position: PositionV1) { + private fun getQueryablesByLocation(position: Position, context: Context) { val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager MainScope().launch { - val result = locateQueryables(cm, server, position) - if (result.error != null) { - Log.e("Results.location", "$result") - showError(result.error) - } else { - updateItems(unmarshallQueryablesResponse(result.stream!!)!!) + try { + val repository = OnlineRepository() + val result = repository.locateQueryables(cm, position, context) + updateItems(result) + } catch (e: ErrorResponseError) { + Log.w("Suggestion", "$e") + showError(e.error) } } } @@ -170,10 +169,10 @@ binding.errorText.text = getString(error.stringResource) binding.errorImage.setImageDrawable(AppCompatResources.getDrawable(this, error.imageResource)) } - private fun updateItems(queryables: List<QueryableV1>) { + private fun updateItems(queryables: List<Queryable>?) { binding.resultsProgress.visibility = View.GONE adapter.update(queryables) - if (queryables.isEmpty()) { + if (queryables.isNullOrEmpty()) { binding.errorImage.visibility = View.VISIBLE binding.errorText.visibility = View.VISIBLE binding.resultsRecycler.visibility = View.GONE @@ -193,16 +192,6 @@ binding.resultsOverlay.visibility = View.GONE binding.errorImage.visibility = View.GONE binding.errorText.visibility = View.GONE binding.resultsRecycler.visibility = View.VISIBLE - } - } - - private suspend fun unmarshallQueryablesResponse(stream: InputStream): List<QueryableV1>? { - return withContext(Dispatchers.IO) { - when (val response = withContext(Dispatchers.IO) {QueryablesResponse.unmarshal(stream)}) { - is QueryablesResponseDev -> response.queryables - is QueryablesResponseV1 -> response.queryables - else -> null - } } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/trolleybus_black.xml b/app/src/main/res/drawable/trolleybus_black.xml new file mode 100644 index 0000000000000000000000000000000000000000..8aad71f63054311cbd6c4b418cc42c5bde75f517 --- /dev/null +++ b/app/src/main/res/drawable/trolleybus_black.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M7.456,2L7.456,3.363L11.773,3.363L11.084,4.743C7.501,4.82 4.729,5.432 4.729,8.367v9.088c0,0.8 0.354,1.518 0.909,2.018v1.617C5.638,21.591 6.047,22 6.547,22h0.909c0.5,0 0.909,-0.409 0.909,-0.909v-0.909h7.271v0.909C15.635,21.591 16.044,22 16.544,22h0.909c0.5,0 0.909,-0.409 0.909,-0.909v-1.617c0.554,-0.5 0.909,-1.218 0.909,-2.018L19.271,8.367c0,-2.795 -2.511,-3.486 -5.845,-3.612 -0.173,-0.009 -0.347,-0.015 -0.525,-0.02l0.691,-1.372h2.954L16.544,2ZM6.547,8.367L17.453,8.367L17.453,12.912L6.547,12.912ZM7.91,15.638c0.754,0 1.363,0.609 1.363,1.363 0,0.754 -0.609,1.363 -1.363,1.363 -0.754,0 -1.363,-0.609 -1.363,-1.363 0,-0.754 0.609,-1.363 1.363,-1.363zM16.09,15.638c0.754,0 1.363,0.609 1.363,1.363 0,0.754 -0.609,1.363 -1.363,1.363 -0.754,0 -1.363,-0.609 -1.363,-1.363 0,-0.754 0.609,-1.363 1.363,-1.363z" + android:strokeWidth="0.908849" + android:fillColor="#000000"/> +</vector>