Bimba.git

commit 009e40933320a46978bb4175a788e8a77e8390c9

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>