Author: Adam Evyčędo <git@apiote.xyz>
merge develop into master for version 3.8.0
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/README.adoc b/README.adoc index 9fd6689891dbe5915ae419d699d378944a4df0c4..fe4569a3272a921f38a58dc15f2bb9419b79f70b 100644 --- a/README.adoc +++ b/README.adoc @@ -4,7 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later = Bimba Adam Evyčędo <me@apiote.xyz> -v3.7.1 2024-11-30 +v3.8.0 2025-01-21 :toc: Bimba is a FLOSS public transport passenger companion; a timetable in your pocket. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fa1aa641706e77412d05ae850be974e2a8165252..fb4153400d1c18791feafd90c302dae996120efe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,10 @@ + +import com.android.build.gradle.internal.tasks.factory.dependsOn +import com.android.build.gradle.tasks.ExtractDeepLinksTask +import com.android.build.gradle.tasks.MergeResources +import de.undercouch.gradle.tasks.download.Download +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + // SPDX-FileCopyrightText: Adam Evyčędo // // SPDX-License-Identifier: GPL-3.0-or-later @@ -7,6 +14,8 @@ id("com.android.application") kotlin("android") kotlin("plugin.parcelize") kotlin("plugin.serialization") + id("org.openapi.generator") + id("de.undercouch.download") } android { @@ -19,8 +28,8 @@ defaultConfig { applicationId = "xyz.apiote.bimba.czwek" minSdk = 21 targetSdk = 35 - versionCode = 33 - versionName = "3.7.1" + versionCode = 34 + versionName = "3.8.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" resourceConfigurations += listOf("en", "de", "en-rGB", "en-rUS", "et", "fr", "it", "pl") @@ -49,6 +58,35 @@ viewBinding = true } } +tasks.register<Download>("downloadFile") { + src("https://bimba.app/transitous/openapi.yml") + dest("${layout.buildDirectory.get()}/motis2.yml") + overwrite(false) +} + +tasks.named("openApiGenerate").dependsOn("downloadFile") + +openApiGenerate { + generatorName.set("kotlin") + inputSpec.set("${layout.buildDirectory.get()}/motis2.yml") + outputDir.set("${layout.buildDirectory.get()}/generated") + apiPackage.set("xyz.apiote.bimba.czwek.api.transitous.api") + invokerPackage.set("xyz.apiote.bimba.czwek.api.transitous.invoker") + modelPackage.set("xyz.apiote.bimba.czwek.api.transitous.model") +} + +kotlin.sourceSets["main"].kotlin.srcDir("${layout.buildDirectory.get()}/generated/src/main/kotlin") + +tasks.withType<KotlinCompile> { + dependsOn("openApiGenerate") +} +tasks.withType<MergeResources> { + dependsOn("openApiGenerate") +} +tasks.withType<ExtractDeepLinksTask> { + dependsOn("openApiGenerate") +} + dependencies { implementation("androidx.core:core-ktx:1.15.0") implementation("androidx.appcompat:appcompat:1.7.0") @@ -76,10 +114,11 @@ implementation("com.google.guava:guava:33.3.1-android") implementation(project(":fruchtfleisch")) implementation("ch.acra:acra-http:5.11.4") implementation("ch.acra:acra-notification:5.11.4") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.moshi:moshi-kotlin:1.15.1") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3") - testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.2.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") } 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 bdb383c7bb383976507ceeed0a8db083413fef7f..4ff3d6d839d708b8e84d21b774678bd6a4e2dc97 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 @@ -11,6 +11,8 @@ import android.net.NetworkCapabilities import android.os.Build import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.openapitools.client.infrastructure.ServerError +import org.openapitools.client.infrastructure.ServerException import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings import java.io.IOException @@ -52,7 +54,11 @@ } data class Result(val stream: InputStream?, val error: Error?) -data class Error(val statusCode: Int, val stringResource: Int, val imageResource: Int) +data class Error(val statusCode: Int, val stringResource: Int, val imageResource: Int) { + companion object { + fun fromTransitous(e: ServerException): Error = Error(e.statusCode, R.string.error, R.drawable.error_other) + } +} suspend fun getBimba(context: Context, server: Server): Result { return try { 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 62d9c8a13101ed601d9234c19ba9b2b8551b6f9b..ae6cebc06ca9353f7d885b38d8fa055eb25c6ec0 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 @@ -11,6 +11,10 @@ import org.yaml.snakeyaml.Yaml import xyz.apiote.bimba.czwek.api.structs.VehicleStatusV1 import xyz.apiote.fruchtfleisch.Reader import java.io.InputStream +import java.time.OffsetDateTime +import java.time.Period +import java.time.ZoneId +import java.time.ZonedDateTime import kotlin.reflect.KClass class TrafficFormatException(override val message: String) : IllegalArgumentException() @@ -185,6 +189,11 @@ data class Time( val Hour: UInt, val Minute: UInt, val Second: UInt, val DayOffset: Byte, val Zone: String ) { companion object { + fun fromOffsetTime(t: OffsetDateTime, zone: ZoneId): Time { + val now = ZonedDateTime.now() + val zonedTime = t.atZoneSameInstant(zone) + return Time(zonedTime.hour.toUInt(), zonedTime.minute.toUInt(), zonedTime.second.toUInt(), Period.between(now.toLocalDate(), zonedTime.toLocalDate()).days.toByte(), zone.id) + } fun unmarshal(stream: InputStream): Time { val reader = Reader(stream) return Time( diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitous.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitous.kt deleted file mode 100644 index e566210b9774cd999d009f32c9b501433dfdf403..0000000000000000000000000000000000000000 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitous.kt +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: Adam Evyčędo -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -package xyz.apiote.bimba.czwek.api - -import android.content.Context -import android.os.Build -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import xyz.apiote.bimba.czwek.R -import java.io.IOException -import java.net.HttpURLConnection -import java.net.URL - -suspend fun askTransitous(context: Context, json: String): Result { - return withContext(Dispatchers.IO) { - val url = URL("https://routing.spline.de/api/") - val c = (url.openConnection() as HttpURLConnection).apply { - setRequestProperty( - "User-Agent", - "${context.getString(R.string.applicationId)}/${context.getString(R.string.versionName)} (${Build.VERSION.SDK_INT})" - ) - addRequestProperty("Content-Type", "application/json") - requestMethod = "POST" - doOutput = true - outputStream.write(json.toByteArray()) - } - try { - if (c.responseCode == 200) { - Result(c.inputStream, null) - } else { - val (string, image) = mapHttpError(c.responseCode) - Result(c.errorStream, Error(c.responseCode, string, image)) - } - } catch (e: IOException) { - Result(null, Error(0, R.string.error_connecting, R.drawable.error_server)) - } - } -} diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt index 3c52446a49d0db59620b89fa7ac64a29bd774cf5..67bbcb416081b6771ed5de98a157adff8460c249 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt @@ -5,13 +5,11 @@ package xyz.apiote.bimba.czwek.api import android.content.Context -import android.util.JsonReader -import android.util.JsonToken import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import xyz.apiote.bimba.czwek.R -import xyz.apiote.bimba.czwek.api.responses.ErrorResponse +import xyz.apiote.bimba.czwek.api.transitous.api.TimetableApi import xyz.apiote.bimba.czwek.repo.Colour import xyz.apiote.bimba.czwek.repo.CongestionLevel import xyz.apiote.bimba.czwek.repo.Event @@ -24,264 +22,87 @@ import xyz.apiote.bimba.czwek.repo.StopEvents import xyz.apiote.bimba.czwek.repo.TrafficResponseException import xyz.apiote.bimba.czwek.repo.Vehicle import xyz.apiote.bimba.czwek.units.Mps -import java.security.MessageDigest +import java.math.BigDecimal import java.time.Instant import java.time.LocalDate +import java.time.OffsetDateTime import java.time.ZoneId -import java.time.ZonedDateTime -@OptIn(ExperimentalStdlibApi::class) suspend fun getTransitousDepartures( context: Context, stop: String, date: LocalDate?, - limit: Int? + limit: Int?, + exact: Boolean ): StopEvents { if (!isNetworkAvailable(context)) { throw TrafficResponseException(0, "", Error(0, R.string.error_offline, R.drawable.error_net)) } - // TODO shouldn't it be start-of-day in stop's timezone? - val timestamp = - date?.atStartOfDay(ZoneId.systemDefault())?.toEpochSecond() ?: ZonedDateTime.now() - .toEpochSecond() - - val json = """ - { - "destination": { - "type": "Module", - "target": "/railviz/get_station" - }, - "content_type": "RailVizStationRequest", - "content": { - "station_id": "$stop", - "time": $timestamp, - "event_count": ${limit ?: 12}, - "direction": "LATER", - "by_schedule_time": false - } + return withContext(Dispatchers.IO) { + // TODO shouldn't it be start-of-day in stop's timezone? + val datetime = date?.let { + OffsetDateTime.of(date.atStartOfDay(), ZoneId.systemDefault().rules.getOffset(Instant.now())) } - """ - - val result = askTransitous(context, json) - - if (result.error != null) { - if (result.stream != null) { - val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) } - throw TrafficResponseException(result.error.statusCode, response.message, result.error) - } else { - throw TrafficResponseException(result.error.statusCode, "", result.error) - } - } else { - return withContext(Dispatchers.IO) { - val events = mutableListOf<Event>() - var stopID = "" - var stopName = "" - var latitude = 0.0 - var longitude = 0.0 - val r = JsonReader(result.stream!!.bufferedReader()) - r.withObject { name -> - if (name != "content") { - r.skipValue() - return@withObject - } - r.withObject { name -> - when (name) { - "events" -> { - r.withArray { - var reason = "" - var eventTimestamp: Long = 0 - var valid = false - var tripID = "" - var idLineID = "" - var stationID = "" - var targetStationID = "" - var targetTimestamp = 0 - var idTimestamp = 0 - var trainNr = 0 - var clasz = -1 - var direction = "" - var lineID = "" - var lineName = "" - var lineColour = "" - var eventType = "" - r.withObject { name -> - when (name) { - "event" -> { - r.withObject { name -> - when (name) { - "reason" -> reason = r.nextString() - "schedule_time" -> r.skipValue() - "schedule_track" -> r.skipValue() - "time" -> eventTimestamp = r.nextLong() - "track" -> r.skipValue() - "valid" -> valid = r.nextBoolean() - else -> r.skipValue() - } - } - } - - "trips" -> { - r.withArray { i -> - if (i == 0) { - r.withObject { name -> - when (name) { - "id" -> { - r.withObject { name -> - when (name) { - "id" -> tripID = r.nextString() - "line_id" -> idLineID = r.nextString() - "station_id" -> stationID = r.nextString() - "target_station_id" -> targetStationID = r.nextString() - "target_time" -> targetTimestamp = r.nextInt() - "time" -> idTimestamp = r.nextInt() - "train_nr" -> trainNr = r.nextInt() - else -> r.skipValue() - } - } - } - - "transport" -> { - r.withObject { name -> - Log.d("WithObject", "next is ${name}") - when (name) { - "clasz" -> clasz = r.nextInt() - "direction" -> direction = r.nextString() - "line_id" -> lineID = r.nextString() - "name" -> lineName = r.nextString() - "provider" -> r.skipValue() - "provider_url" -> r.skipValue() - "range" -> r.skipValue() - "route_color" -> lineColour = r.nextString() - "route_text_color" -> r.skipValue() - else -> r.skipValue() - } - } - } - else -> r.skipValue() - } - } - } else { - r.skipValue() - } - } - } - - "type" -> { - eventType = r.nextString() - } - else -> r.skipValue() - } - } - if (eventType == "ARR") { - val hash = MessageDigest.getInstance("SHA-256").let { - it.update(tripID.toByteArray()) - it.update(idLineID.toByteArray()) - it.update(stationID.toByteArray()) - it.update(targetStationID.toByteArray()) - it.update(targetTimestamp.toHexString().toByteArray()) - it.update(idTimestamp.toHexString().toByteArray()) - it.update(trainNr.toHexString().toByteArray()) - it.digest() - }.toHexString() - val t = - ZonedDateTime.ofInstant( - Instant.ofEpochSecond(eventTimestamp), - ZoneId.systemDefault() - ) - events.add( - Event( - id = hash, - arrivalTime = Time( - t.hour.toUInt(), - t.minute.toUInt(), - t.second.toUInt(), - (t.dayOfYear - ZonedDateTime.now().dayOfYear).toByte(), - ZoneId.systemDefault().id - ), - departureTime = Time( - t.hour.toUInt(), - t.minute.toUInt(), - t.second.toUInt(), - (t.dayOfYear - ZonedDateTime.now().dayOfYear).toByte(), - ZoneId.systemDefault().id - ), - status = 0u, - isRealtime = reason in setOf("IS", "PROPAGATION", "FORECAST"), - vehicle = Vehicle( - "", - Position(0.0, 0.0), - 0u, - Mps(0), - LineStub( - lineName, - LineType.fromTransitous(clasz), - Colour.fromHex(lineColour) - ), - direction, - CongestionLevel.UNKNOWN, - OccupancyStatus.UNKNOWN - ), - boarding = 0xffu, - alerts = emptyList(), - exact = true, - terminusArrival = !valid - ) - ) - } - } - } - - "station" -> { - r.withObject { name -> - when (name) { - "id" -> stopID = r.nextString() - "name" -> stopName = r.nextString() - "pos" -> { - r.withObject { posName -> - when (posName) { - "lat" -> latitude = r.nextDouble() - "lng" -> longitude = r.nextDouble() - } - } - } - } - } - } - else -> r.skipValue() - } - } + val times = TimetableApi().stoptimes(stop, limit ?: 12, datetime) + var stopName = "" + var latitude: BigDecimal = BigDecimal(0) + var longitude: BigDecimal = BigDecimal(0) + val departures = times.stopTimes.filter { + !exact || stop == it.place.stopId + }.map { + Log.i("stop", "stopID recvd: ${it.place.stopId}") + if ((it.place.departure ?: it.place.arrival) == null) { + null + } else { + latitude = it.place.lat + longitude = it.place.lon + stopName = it.place.name + Event( + it.tripId + it.source, + it.place.arrival?.let { + Time.fromOffsetTime(it, ZoneId.systemDefault()) + }, + it.place.departure?.let { + Time.fromOffsetTime(it, ZoneId.systemDefault()) + }, + 0u, + it.realTime, + Vehicle( + it.tripId, + Position(0.0, 0.0), + 0u, + Mps(0), + LineStub( + it.routeShortName, + LineType.fromTransitous2(it.mode), + Colour.fromHex(it.routeColor) + ), + it.headsign, + CongestionLevel.UNKNOWN, + OccupancyStatus.UNKNOWN + ), + boarding = 0xffu, + alerts = emptyList(), + exact = true, + terminusArrival = it.place.departure == null + ) } - - return@withContext StopEvents( - events, - Stop(stopID, stopName, stopName, "", "transitous", Position(latitude, longitude), listOf()), - listOf() - ) - } - } -} - -fun JsonReader.withObject(f: (String) -> Unit) { - beginObject() - while (hasNext()) { - try { - f(nextName()) - } catch (e: IllegalStateException) { - val a: JsonToken = this.peek() - Log.d("WithObject", "next is ${a.name} ${nextString()}") - throw e - } + }.filterNotNull() + Log.i("stop", "stopID asked: $stop") + StopEvents( + departures, + Stop( + stop, + stopName, + stopName, + "", + "transitous", + Position(latitude.toDouble(), longitude.toDouble()), + listOf(), + null + ), + listOf() + ) } - endObject() -} - -fun JsonReader.withArray(f: (Int) -> Unit) { - beginArray() - var i = 0 - while (hasNext()) { - f(i) - i++ - } - endArray() } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousQueryables.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousQueryables.kt index 71d32872c65e42a1c27a8115ccf99bb3e4cdb819..a3cbf1b7295607079700a2e1071fee8704558e31 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousQueryables.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousQueryables.kt @@ -5,192 +5,82 @@ package xyz.apiote.bimba.czwek.api import android.content.Context -import android.util.JsonReader import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import xyz.apiote.bimba.czwek.R -import xyz.apiote.bimba.czwek.api.responses.ErrorResponse +import xyz.apiote.bimba.czwek.api.transitous.api.GeocodeApi +import xyz.apiote.bimba.czwek.api.transitous.api.MapApi +import xyz.apiote.bimba.czwek.api.transitous.model.Match import xyz.apiote.bimba.czwek.repo.Position import xyz.apiote.bimba.czwek.repo.Queryable import xyz.apiote.bimba.czwek.repo.Stop import xyz.apiote.bimba.czwek.repo.TrafficResponseException import xyz.apiote.bimba.czwek.units.DistanceUnit +import xyz.apiote.bimba.czwek.units.Km import xyz.apiote.bimba.czwek.units.Metre +import java.util.Locale +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.cos + +val MetresPerDegreeLatitude = Metre(111320.0) suspend fun getTransitousQueryables(query: String, context: Context): List<Queryable> { if (!isNetworkAvailable(context)) { throw TrafficResponseException(0, "", Error(0, R.string.error_offline, R.drawable.error_net)) } - val json = """ - { - "destination": { - "type": "Module", - "target": "/guesser" - }, - "content_type": "StationGuesserRequest", - "content": { - "input": "$query", - "guess_count": 24 - } - } - """ - val result = askTransitous(context, json) + return withContext(Dispatchers.IO) { + GeocodeApi().geocode(query, Locale.getDefault().language).filter { it.type == Match.Type.STOP } + .map { Stop(it) } + } +} - if (result.error != null) { - if (result.stream != null) { - val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) } - throw TrafficResponseException(result.error.statusCode, response.message, result.error) - } else { - throw TrafficResponseException(result.error.statusCode, "", result.error) - } +suspend fun locateTransitousQueryables( + br: Position, + tl: Position, + context: Context +): List<Queryable> { + if (!isNetworkAvailable(context)) { + throw TrafficResponseException(0, "", Error(0, R.string.error_offline, R.drawable.error_net)) + } + + val dLat = abs(br.latitude - tl.latitude) / 2 + var dLon = abs(br.longitude - tl.longitude) / 2 + val centre = Position(abs(br.latitude + tl.latitude) / 2, abs(br.longitude + tl.longitude) / 2) + + val latitudeLimit = Km(10.0).meters()/MetresPerDegreeLatitude.meters() // ~radius in degrees latitude + val corners = if (dLat > latitudeLimit) { + dLon = dLon * latitudeLimit / dLat + val newBr = Position(centre.latitude-latitudeLimit, centre.longitude+dLon) + val newTL = Position(centre.latitude+latitudeLimit, centre.longitude-dLon) + Pair(newBr.toString(), newTL.toString()) } else { - return withContext(Dispatchers.IO) { - val queryables = mutableListOf<Queryable>() - val r = JsonReader(result.stream!!.bufferedReader()) - r.beginObject() - while (r.hasNext()) { - val rootFieldName = r.nextName() // content - if (rootFieldName == "content") { - r.beginObject() - r.nextName() // guesses - r.beginArray() - while (r.hasNext()) { - var id = "" - var name = "" - var latitude = 0.0 - var longitude = 0.0 - r.beginObject() - while (r.hasNext()) { - val fieldName = r.nextName() - when (fieldName) { - "id" -> id = r.nextString() - "name" -> name = r.nextString() - "pos" -> { - r.beginObject() - while (r.hasNext()) { - val positionFieldName = r.nextName() - when (positionFieldName) { - "lat" -> latitude = r.nextDouble() - "lng" -> longitude = r.nextDouble() - } - } - r.endObject() - } - } - } - r.endObject() - queryables.add( - Stop( - code = id, - name = name, - nodeName = name, - zone = "", - feedID = "transitous", - position = Position(latitude = latitude, longitude = longitude), - changeOptions = emptyList() - ) - ) - } - r.endArray() - r.endObject() - } else { - r.skipValue() - } - } - r.endObject() + Pair(br.toString(), tl.toString()) + } - return@withContext queryables - } + return withContext(Dispatchers.IO) { + MapApi().stops(corners.first, corners.second).filter { it.stopId != null }.map { Stop(it) } } } -suspend fun locateTransitousQueryables(position: Position, context: Context, radius: DistanceUnit = Metre(500.0)): List<Queryable> { +suspend fun locateTransitousQueryables( + position: Position, + context: Context, + radius: DistanceUnit = Metre(500.0) +): List<Queryable> { if (!isNetworkAvailable(context)) { throw TrafficResponseException(0, "", Error(0, R.string.error_offline, R.drawable.error_net)) } - val json = """ - { - "destination": { - "type": "Module", - "target": "/lookup/geo_station" - }, - "content_type": "LookupGeoStationRequest", - "content": { - "max_radius": ${radius.meters().toInt()}, - "pos": { - "lat": ${position.latitude}, - "lng": ${position.longitude} - } - } + val deltaLatitude = radius.meters() / MetresPerDegreeLatitude.meters() + val deltaLongitude = radius.meters() / (cos(position.latitude * PI / 180) / 360 * 40075000) + val br = Position(position.latitude - deltaLatitude, position.longitude + deltaLongitude) + val tl = Position(position.latitude + deltaLatitude, position.longitude - deltaLongitude) + return locateTransitousQueryables(br, tl, context).map { + when (it) { + is Stop -> it + else -> null } - """ - val result = askTransitous(context, json) - - if (result.error != null) { - if (result.stream != null) { - val response = withContext(Dispatchers.IO) { ErrorResponse.unmarshal(result.stream) } - throw TrafficResponseException(result.error.statusCode, response.message, result.error) - } else { - throw TrafficResponseException(result.error.statusCode, "", result.error) - } - } else { - return withContext(Dispatchers.IO) { - val queryables = mutableListOf<Queryable>() - val r = JsonReader(result.stream!!.bufferedReader()) - r.beginObject() - while (r.hasNext()) { - val rootFieldName = r.nextName() // content - if (rootFieldName == "content") { - r.beginObject() - r.nextName() // guesses - r.beginArray() - while (r.hasNext()) { - var id = "" - var name = "" - var latitude = 0.0 - var longitude = 0.0 - r.beginObject() - while (r.hasNext()) { - val fieldName = r.nextName() - when (fieldName) { - "id" -> id = r.nextString() - "name" -> name = r.nextString() - "pos" -> { - r.beginObject() - while (r.hasNext()) { - val positionFieldName = r.nextName() - when (positionFieldName) { - "lat" -> latitude = r.nextDouble() - "lng" -> longitude = r.nextDouble() - } - } - r.endObject() - } - } - } - r.endObject() - queryables.add( - Stop( - code = id, - name = name, - nodeName = name, - zone = "", - feedID = "transitous", - position = Position(latitude = latitude, longitude = longitude), - changeOptions = emptyList() - ) - ) - } - r.endArray() - r.endObject() - } else { - r.skipValue() - } - } - r.endObject() - return@withContext queryables - } - } + }.filterNotNull().sortedWith(Stop.distanceComparator(position)) } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousSettings.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousSettings.kt deleted file mode 100644 index b135a0c210d068dcde94ba25b825e54ded1ea350..0000000000000000000000000000000000000000 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/transitousSettings.kt +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: Adam Evyčędo -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -package xyz.apiote.bimba.czwek.api - -import android.content.Context -import androidx.preference.PreferenceManager - -fun isTransitousEnabled(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("transitous_enabled", false) -} 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 def65cf9bf6e3e4d1f37c37e65d5121123a5824d..361ea8f9903ab8ab086365dd4bcaf666b0020aef 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeViewModel.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import org.openapitools.client.infrastructure.ServerException import xyz.apiote.bimba.czwek.repo.Event import xyz.apiote.bimba.czwek.repo.Favourite import xyz.apiote.bimba.czwek.repo.FeedInfo @@ -45,6 +46,9 @@ try { getFeeds(context) mutableQueryables.value = OnlineRepository().queryQueryables(query, context) ?: emptyList() } catch (e: TrafficResponseException) { + // XXX intentionally no error showing in suggestions + Log.e("Suggestion", "$e") + } catch (e: ServerException) { // XXX intentionally no error showing in suggestions Log.e("Suggestion", "$e") } @@ -86,22 +90,29 @@ favourite.feedID, favourite.stopCode, null, context, - 12 // XXX heuristics + 12, // XXX heuristics + favourite.exact ) - stopDepartures?.let { sDs -> - if (sDs.events.isEmpty()) { - Pair(favourite.feedID+favourite.stopCode, Optional.empty()) - } else { - Pair(favourite.feedID+favourite.stopCode, Optional.ofNullable(sDs.events.find { departure -> - favourite.lines.isEmpty() or favourite.lines.contains( - departure.vehicle.Line.name - ) - })) - } - } ?: Pair(favourite.feedID+favourite.stopCode, Optional.empty()) + stopDepartures?.let { sDs -> + if (sDs.events.isEmpty()) { + Pair(favourite.feedID + favourite.stopCode, Optional.empty()) + } else { + Pair( + favourite.feedID + favourite.stopCode, + Optional.ofNullable(sDs.events.find { departure -> + favourite.lines.isEmpty() or favourite.lines.contains( + departure.vehicle.Line.name + ) + }) + ) + } + } ?: Pair(favourite.feedID + favourite.stopCode, Optional.empty()) } catch (e: TrafficResponseException) { Log.w("DeparturesForFavourite", "$e") - Pair(favourite.feedID+favourite.stopCode, Optional.empty()) + Pair(favourite.feedID + favourite.stopCode, Optional.empty()) + } catch (e: ServerException) { + Log.w("DeparturesForFavourite", "Transitous returned ${e.statusCode}, ${e.message}") + Pair(favourite.feedID + favourite.stopCode, Optional.empty()) } } }.awaitAll().associate { it } 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 a9ff0719d69c398d56d1f3e06ad2aa3cc8072187..ceeb223f0fc4e4f66c0e9c81fda2a79c08c586f0 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 @@ -168,7 +168,7 @@ private fun showStop(content: View, stop: Stop) { context?.let { ctx -> content.findViewById<TextView>(R.id.stop_name).text = stop.name content.findViewById<Button>(R.id.departures_button).setOnClickListener { - startActivity(DeparturesActivity.getIntent(requireContext(), stop.code, stop.name, stop.feedID!!)) + startActivity(DeparturesActivity.getIntent(requireContext(), stop.code, stop.name, stop.feedID!!, true)) } content.findViewById<Button>(R.id.navigation_button).setOnClickListener { try { 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 01f592aa9c53a29de8ef478f6d1ff9f504d97335..a5d676e2b8392b6aca5f6efb899b86d44fff2a97 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 @@ -381,37 +381,41 @@ } private fun setContent(view: View, ctx: Context, updating: Boolean = false) { view.apply { + val arrivalStatus = findViewById<TextView>(R.id.arrival_status) + val arrivalTime = findViewById<TextView>(R.id.arrival_time) + val departureStatus = findViewById<TextView>(R.id.departure_status) + val departureTime = findViewById<TextView>(R.id.departure_time) if (event.arrivalTime == event.departureTime) { if (!event.exact) { - findViewById<TextView>(R.id.arrival_status).apply { + arrivalStatus.apply { visibility = View.VISIBLE text = context.getString(R.string.approximately) } } else { - findViewById<TextView>(R.id.arrival_status).visibility = View.GONE + arrivalStatus.visibility = View.GONE } findViewById<TextView>(R.id.arrival_time).apply { text = event.arrivalTimeString(ctx) visibility = View.VISIBLE } - findViewById<TextView>(R.id.departure_status).visibility = View.GONE - findViewById<TextView>(R.id.departure_time).visibility = View.GONE + departureStatus.visibility = View.GONE + departureTime.visibility = View.GONE } else { if (event.arrivalTime != null) { - findViewById<TextView>(R.id.arrival_time).visibility = View.VISIBLE - findViewById<TextView>(R.id.arrival_time).text = event.arrivalTimeString(ctx) - findViewById<TextView>(R.id.arrival_status).visibility = View.VISIBLE - findViewById<TextView>(R.id.arrival_status).text = if (!event.exact) { + arrivalTime.visibility = View.VISIBLE + arrivalTime.text = event.arrivalTimeString(ctx) + arrivalStatus.visibility = View.VISIBLE + arrivalStatus.text = if (!event.exact) { context?.getString(R.string.arrival_approximate) } else { context?.getString(R.string.arrival) } } if (event.departureTime != null) { - findViewById<TextView>(R.id.departure_time).visibility = View.VISIBLE - findViewById<TextView>(R.id.departure_time).text = event.departureTimeString(ctx) - findViewById<TextView>(R.id.departure_status).visibility = View.VISIBLE - findViewById<TextView>(R.id.departure_status).text = if (!event.exact) { + departureTime.visibility = View.VISIBLE + departureTime.text = event.departureTimeString(ctx) + departureStatus.visibility = View.VISIBLE + departureStatus.text = if (!event.exact) { context?.getString(R.string.departure_approximate) } else { context?.getString(R.string.departure) @@ -633,4 +637,4 @@ } } return content } -} \ No newline at end of file +} 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 8ca35eb6214c46644d0d28ee8f635bd4054a37d5..33e28dd4a73142f2adca069d4055549364e04223 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 @@ -64,16 +64,19 @@ const val NAME_PARAM = "name" const val FEED_PARAM = "feedID" const val LINES_FILTER_PARAM = "linesFilter" const val LINE_PARAM = "line" + const val EXACT_PARAM = "exact" fun getIntent( context: Context, code: String, name: String, feedID: String, - ) = Intent(context, DeparturesActivity::class.java).apply { + exact: Boolean = false, + ) = Intent(context, DeparturesActivity::class.java).apply { putExtra(CODE_PARAM, code) putExtra(NAME_PARAM, name) putExtra(FEED_PARAM, feedID) + putExtra(EXACT_PARAM, exact) } fun getIntent( @@ -342,9 +345,7 @@ timePickerStart!!.show(supportFragmentManager, "timePickerStart") true } - /* TODO elizabeth R.id.terminus_arrival_showing -> { - // TODO get array from R.arrays val options = arrayOf( TERMINUS_ARRIVAL_GREY_OUT, TERMINUS_ARRIVAL_HIDE, @@ -368,7 +369,7 @@ } .setNegativeButton(R.string.cancel) { _, _ -> } .show() true - }*/ + } else -> super.onOptionsItemSelected(it) } @@ -465,6 +466,8 @@ } } private fun getCode() = intent?.extras?.getString(CODE_PARAM) + + private fun getExact() = intent?.extras?.getBoolean(EXACT_PARAM) == true fun getDepartures(force: Boolean = false) { binding.departuresUpdatesProgress.isIndeterminate = true @@ -474,7 +477,7 @@ } else { adapter.refreshItems() setupSnackbar() } - viewModel.getDepartures(this, viewModel.date, force) + viewModel.getDepartures(this, viewModel.date, force, getExact()) } private fun setupSnackbar() { @@ -587,7 +590,8 @@ feedID, feedName, code, getName(), - linesFilter.toList() + linesFilter.toList(), + getExact() )).copy(lines = linesFilter.toList()) repo.saveFavourite(favourite) repo.close() diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt index 21dbd04eefa862f93ad0f5ad95a0fc1ef48e80db..613aca9107d7bfebb31aed2d6189f045e0625f1b 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt @@ -5,6 +5,7 @@ package xyz.apiote.bimba.czwek.departures import android.app.Activity +import xyz.apiote.bimba.czwek.R import android.content.Context import android.content.Intent import android.net.Uri @@ -14,12 +15,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +import org.openapitools.client.infrastructure.ServerException import xyz.apiote.bimba.czwek.api.Error import xyz.apiote.bimba.czwek.api.mapHttpError import xyz.apiote.bimba.czwek.repo.FeedInfo import xyz.apiote.bimba.czwek.repo.OfflineRepository import xyz.apiote.bimba.czwek.repo.OnlineRepository import xyz.apiote.bimba.czwek.repo.QrLocation +import xyz.apiote.bimba.czwek.repo.Queryable import xyz.apiote.bimba.czwek.repo.StopEvents import xyz.apiote.bimba.czwek.repo.TrafficResponseException import java.time.LocalDate @@ -46,7 +49,7 @@ // TODO observe in activity, refreshing is not enough var date: LocalDate? = null - fun getDepartures(context: Context, date: LocalDate?, force: Boolean) { + fun getDepartures(context: Context, date: LocalDate?, force: Boolean, exact: Boolean) { MainScope().launch { try { if (feed == null) { @@ -61,7 +64,8 @@ feed!!.id, code, date, context, - requestedItemsNumber + requestedItemsNumber, + exact ) stopDepartures?.let { if (stopDepartures.events.isEmpty()) { @@ -75,6 +79,11 @@ if (!departures.isInitialized || force) { _error.value = e.error } Log.w("Departures", "$e") + } catch (e: ServerException) { + if (!departures.isInitialized || force) { + _error.value = Error.fromTransitous(e) + } + Log.w("Departures", "Transitous returned ${e.statusCode}, ${e.message}") } } } @@ -146,7 +155,9 @@ } } ?: throw TrafficResponseException(41) } - null -> intent?.extras?.getString(DeparturesActivity.CODE_PARAM) ?: throw TrafficResponseException(41) + null -> intent?.extras?.getString(DeparturesActivity.CODE_PARAM) + ?: throw TrafficResponseException(41) + else -> throw TrafficResponseException(41) } } 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 index 4b740bd0b13df60606a1ab0eec3e0f1e884a2a39..01fea09a4f4c194afb4d5092864db448f821989b 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Colour.kt @@ -18,7 +18,8 @@ return rgb } companion object { - fun fromHex(hex: String): Colour { + fun fromHex(hex: String?): Colour { + if (hex == null) return Colour(255u, 255u, 255u) return hex.removePrefix("#").let { if (it.isEmpty()) { Colour(255u, 255u, 255u) diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Favourite.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Favourite.kt index fa0a08da12719f5e4c70a44f5f002a995adbeb45..e646289182e4bc43f395baa22349625c864941a3 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Favourite.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Favourite.kt @@ -4,4 +4,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later package xyz.apiote.bimba.czwek.repo -data class Favourite(val sequence: Int?, val feedID: String, val feedName: String, val stopCode: String, val stopName: String, val lines: List<String>) \ No newline at end of file +data class Favourite( + val sequence: Int?, + val feedID: String, + val feedName: String, + val stopCode: String, + val stopName: String, + val lines: List<String>, + val exact: Boolean +) \ 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 index e612503b10f270a49961d1482475da55fb3f47db..9b8b1da3afd0b34a1f9fb2d2812be70543d2c998 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Interfaces.kt @@ -35,7 +35,8 @@ feedID: String, stop: String, date: LocalDate?, context: Context, - limit: Int? + limit: Int?, + exact: Boolean ): StopEvents? suspend fun getLocatablesIn( 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 index 46be84493816f7bcf0e5e8d25416344dc81b6172..be76479081cc2763d596e2271e83e03096730d91 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineType.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineType.kt @@ -7,6 +7,7 @@ import xyz.apiote.bimba.czwek.api.LineTypeV1 import xyz.apiote.bimba.czwek.api.LineTypeV2 import xyz.apiote.bimba.czwek.api.LineTypeV3 +import xyz.apiote.bimba.czwek.api.transitous.model.Mode enum class LineType { UNKNOWN, TRAM, BUS, TROLLEYBUS, METRO, RAIL, FERRY, CABLE_TRAM, CABLE_CAR, FUNICULAR, MONORAIL, PLANE; @@ -43,22 +44,28 @@ LineTypeV3.MONORAIL -> valueOf("MONORAIL") } } - fun fromTransitous(clasz: Int): LineType { - return when (clasz) { - 0 -> PLANE - 1 -> RAIL - 2 -> RAIL - 3 -> BUS - 4 -> RAIL - 5 -> RAIL - 6 -> RAIL - 7 -> METRO - 8 -> METRO - 9 -> TRAM - 10 -> BUS - 11 -> FERRY - 12 -> UNKNOWN // other - else -> UNKNOWN + fun fromTransitous2(mode: Mode): LineType { + return when(mode) { + Mode.AIRPLANE -> PLANE + Mode.WALK -> UNKNOWN + Mode.BIKE -> UNKNOWN + Mode.CAR -> UNKNOWN + Mode.RENTAL -> UNKNOWN + Mode.TRANSIT -> UNKNOWN + Mode.TRAM -> TRAM + Mode.SUBWAY -> METRO + Mode.FERRY -> FERRY + Mode.METRO -> METRO + Mode.BUS -> BUS + Mode.COACH -> BUS + Mode.RAIL -> RAIL + Mode.HIGHSPEED_RAIL -> RAIL + Mode.LONG_DISTANCE -> RAIL + Mode.NIGHT_RAIL -> RAIL + Mode.REGIONAL_FAST_RAIL -> RAIL + Mode.REGIONAL_RAIL -> RAIL + Mode.OTHER -> UNKNOWN + Mode.CAR_PARKING -> UNKNOWN } } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt index 309d6ec0f00248d543db51e92389dfddb1457a3d..07850536570dac297712700dc046d3805f2912c7 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt @@ -55,7 +55,8 @@ cursor.getString(2), cursor.getString(3), stopCode, cursor.getString(1), - cursor.getString(4).split("||").filter { it != "" } + cursor.getString(4).split("||").filter { it != "" }, + false // TODO get exact from database ) cursor.close() return f @@ -81,7 +82,8 @@ cursor.getString(2), cursor.getString(3), cursor.getString(4), cursor.getString(1), - cursor.getString(5).split("||").filter { it != "" } + cursor.getString(5).split("||").filter { it != "" }, + false // TODO get exact from database ) ) } @@ -199,7 +201,8 @@ feedID: String, stop: String, date: LocalDate?, context: Context, - limit: Int? + limit: Int?, + exact: Boolean ): StopEvents? { TODO("Not yet implemented") } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt index 0b47623ff0b1f191b90a9e19f308f9a50878117f..3f078daac0a5c69f2679a0e00142d3172234aae9 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt @@ -50,7 +50,6 @@ import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV1 import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV2 import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV3 import xyz.apiote.bimba.czwek.api.responses.QueryablesResponseV4 -import xyz.apiote.bimba.czwek.units.Metre import java.time.LocalDate // todo [3.2] in Repository check if responses are BARE or HTML @@ -113,10 +112,11 @@ feedID: String, stop: String, date: LocalDate?, context: Context, - limit: Int? + limit: Int?, + exact: Boolean ): StopEvents? { return if (feedID == "transitous") { - getTransitousDepartures(context, stop, date, limit) + getTransitousDepartures(context, stop, date, limit, exact) } else { val result = xyz.apiote.bimba.czwek.api.getDepartures( context, @@ -174,12 +174,10 @@ bl: Position, tr: Position, ): List<Locatable>? { val transitousQueryables = if (Server.get(context).feeds.transitousEnabled()) { - val centre = Position( - latitude = (bl.latitude + tr.latitude) / 2, - longitude = (bl.longitude + tr.longitude) / 2 - ) locateTransitousQueryables( - centre, context, Metre(centre.distanceTo(bl).coerceAtMost(5000f).toDouble()) + Position(bl.latitude, tr.longitude), + Position(tr.latitude, bl.longitude), + context ).map { when (it) { is Stop -> it 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 index d03cf34ae8cb789df7e078f0f24c509e63343a12..39ec428637d1165e0c41b12da12f7420de27a8e9 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Position.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Position.kt @@ -9,6 +9,7 @@ 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 } @@ -22,4 +23,23 @@ latitude = other.latitude longitude = other.longitude }) } -} \ No newline at end of file + + override fun toString(): String { + return "$latitude,$longitude" + } + + companion object { + fun comparator(centre: Position) = object : Comparator<Position> { + override fun compare( + o1: Position?, + o2: Position? + ): Int { + if (o1 == null || o2 == null) { + throw NullPointerException() + } + + return o1.distanceTo(centre).compareTo(o2.distanceTo(centre)) + } + } + } +} 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 index af81fbf17847b928c5c2ee0c674cb4a451bf858a..8448bc7210a18e9e0d352a657e4ef0db6139848a 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Stop.kt @@ -19,6 +19,8 @@ import xyz.apiote.bimba.czwek.RoundedBackgroundSpan import xyz.apiote.bimba.czwek.api.StopV1 import xyz.apiote.bimba.czwek.api.StopV2 import xyz.apiote.bimba.czwek.api.StopV3 +import xyz.apiote.bimba.czwek.api.transitous.model.Match +import xyz.apiote.bimba.czwek.api.transitous.model.Place data class Stop( @@ -28,8 +30,20 @@ val nodeName: String, val zone: String, val feedID: String?, val position: Position, - val changeOptions: List<ChangeOption> + val changeOptions: List<ChangeOption>, + val description: String? ) : Queryable, Locatable, StopAbstract { + companion object { + fun distanceComparator(centre: Position) = object : Comparator<Stop> { + override fun compare( + o1: Stop?, + o2: Stop? + ): Int { + return Position.comparator(centre).compare(o1?.location(), o2?.location()) + } + + } + } override fun icon(context: Context, scale: Float): Drawable { return super.icon(context, nodeName, scale) @@ -46,7 +60,8 @@ s.name, s.zone, null, Position(s.position), - s.changeOptions.map { ChangeOption(it) }) + s.changeOptions.map { ChangeOption(it) }, + null) constructor(s: StopV2) : this( s.code, @@ -55,7 +70,8 @@ s.nodeName, s.zone, s.feedID, Position(s.position), - s.changeOptions.map { ChangeOption(it) }) + s.changeOptions.map { ChangeOption(it) }, + null) constructor(s: StopV3) : this( s.code, @@ -64,7 +80,30 @@ s.nodeName, s.zone, s.feedID, Position(s.position), - s.changeOptions.map { ChangeOption(it) }) + s.changeOptions.map { ChangeOption(it) }, + null) + + constructor(s: Match): this( + s.id, + s.name, + s.areas.sortedBy { it.adminLevel }.map { it.name }.distinct().joinToString() + s.name, + "", + "transitous", + Position(s.lat.toDouble(), s.lon.toDouble()), + emptyList(), + s.areas.sortedBy { it.adminLevel }.map { it.name }.distinct().joinToString() + ) + + constructor(s: Place): this( + s.stopId!!, + s.name, + s.name, + "", + "transitous", + Position(s.lat.toDouble(), s.lon.toDouble()), + emptyList(), + "" + ) fun changeOptions(context: Context, decoration: LineDecoration): Pair<Spannable, String> { return Pair(changeOptions.groupBy { it.line } 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 6edad804085f30265578b89d4abe728cf3c147c8..2df5391da9da2b9bb1eddecfc5ba8bc2c4568ef9 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 @@ -30,6 +30,7 @@ class BimbaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View = itemView.findViewById(R.id.suggestion) val icon: ImageView = itemView.findViewById(R.id.suggestion_image) val title: TextView = itemView.findViewById(R.id.suggestion_title) + val changeOptions: TextView = itemView.findViewById(R.id.suggestion_change_options) val description: TextView = itemView.findViewById(R.id.suggestion_description) val feedName: TextView = itemView.findViewById(R.id.feed_name) val distance: TextView = itemView.findViewById(R.id.distance) @@ -77,7 +78,7 @@ holder?.icon?.apply { setImageDrawable(stopStub.icon(context!!)) contentDescription = context.getString(R.string.stop_content_description) } - holder?.description?.text = when { + holder?.changeOptions?.text = when { stopStub.zone != "" && stopStub.onDemand -> context?.getString( R.string.stop_stub_on_demand_in_zone, stopStub.zone @@ -119,7 +120,7 @@ holder?.arrow?.apply { setImageResource(R.drawable.arrow) rotation = angle visibility = View.VISIBLE - contentDescription = "Arrow" // TODO + contentDescription = context?.getString(R.string.arrow) ?: "Arrow pointing to the stop" // TODO add angle } holder?.distance?.apply { val us = UnitSystem.getSelected(context!!) @@ -147,12 +148,23 @@ holder?.feedName?.visibility = View.GONE } context?.let { stop.changeOptions(it, Stop.LineDecoration.fromPreferences(context)).let { changeOptions -> - holder?.description?.apply { - text = changeOptions.first - contentDescription = changeOptions.second + if (changeOptions.first.isEmpty()) { + holder?.changeOptions?.visibility = View.GONE + } else { + holder?.changeOptions?.apply { + text = changeOptions.first + contentDescription = changeOptions.second + visibility = View.VISIBLE + } } } } + if (stop.description.isNullOrBlank()) { + holder?.description?.visibility = View.GONE + } else { + holder?.description?.visibility = View.VISIBLE + holder?.description?.text = stop.description + } } private fun bindLine( @@ -172,7 +184,7 @@ holder?.feedName?.visibility = View.VISIBLE holder?.feedName?.text = feeds?.get(line.feedID)?.name ?: "" } holder?.title?.text = line.name - holder?.description?.text = if (line.headsigns.size == 1) { + holder?.changeOptions?.text = if (line.headsigns.size == 1) { context?.getString( R.string.line_headsign, line.headsigns[0].joinToString { it }) @@ -182,7 +194,7 @@ R.string.line_headsigns, line.headsigns[0].joinToString { it }, line.headsigns[1].joinToString { it }) } - holder?.description?.contentDescription = if (line.headsigns.size == 1) { + holder?.changeOptions?.contentDescription = if (line.headsigns.size == 1) { context?.getString( R.string.line_headsign_content_description, line.headsigns[0].joinToString { it }) 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 b29210b31e39602eb0a83b32b219f1d55c41ee7b..0c87cba970f5cce61d4d0d45aec18eef3015dfa7 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 @@ -32,6 +32,7 @@ import com.google.openlocationcode.OpenLocationCode import kotlinx.coroutines.MainScope import kotlinx.coroutines.Runnable import kotlinx.coroutines.launch +import org.openapitools.client.infrastructure.ServerException import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.api.Error import xyz.apiote.bimba.czwek.databinding.ActivityResultsBinding @@ -114,20 +115,20 @@ binding.resultsRecycler.adapter = adapter when (getMode()) { Mode.MODE_LOCATION -> { - supportActionBar?.title = getString(R.string.stops_nearby) + binding.topAppBar.title = getString(R.string.stops_nearby) locate() } Mode.MODE_SHORT_CODE_LOCATION -> { val query = intent.extras?.getString(QUERY_KEY) - supportActionBar?.title = getString(R.string.stops_near_code, query) + binding.topAppBar.title = getString(R.string.stops_near_code, query) shortOLC = OpenLocationCode(query) locate() } Mode.MODE_SHORT_CODE -> { val query = intent.extras?.getString(QUERY_KEY) - supportActionBar?.title = getString(R.string.stops_near_code, query) + binding.topAppBar.title = getString(R.string.stops_near_code, query) val split = query!!.trim().split(" ") val code = split.first().trim(',').trim() val freePart = split.drop(1).joinToString(" ") @@ -147,7 +148,7 @@ Mode.MODE_POSITION -> { val query = intent.extras?.getString(QUERY_KEY) val lat = intent.extras?.getDouble(LATITUDE_KEY) val lon = intent.extras?.getDouble(LONGITUDE_KEY) - supportActionBar?.title = getString(R.string.stops_near_code, query) + binding.topAppBar.title = getString(R.string.stops_near_code, query) getQueryablesByLocation(Location(null).apply { latitude = lat!! longitude = lon!! @@ -156,7 +157,7 @@ } Mode.MODE_SEARCH -> { val query = intent.extras?.getString(QUERY_KEY)!! - supportActionBar?.title = getString(R.string.results_for, query) + binding.topAppBar.title = getString(R.string.results_for, query) getQueryablesByQuery(query, this) } } @@ -270,6 +271,9 @@ updateItems(result, null, false) } catch (e: TrafficResponseException) { Log.w("Suggestion", "$e") showError(e.error) + } catch (e: ServerException) { + Log.w("Suggestion", "Transitous returned: ${e.statusCode}, ${e.message}") + showError(Error.fromTransitous(e)) } } } @@ -289,6 +293,9 @@ updateItems(result, position, showArrow) } catch (e: TrafficResponseException) { Log.w("Suggestion", "$e") showError(e.error) + } catch (e: ServerException) { + Log.w("Suggestion", "Transitous returned: ${e.statusCode}, ${e.message}") + showError(Error.fromTransitous(e)) } } } diff --git a/app/src/main/res/layout/departure_bottom_sheet.xml b/app/src/main/res/layout/departure_bottom_sheet.xml index f2bb7dc23101df9ee440b0a8997d2ebbfbe753ce..8fb3f89430cb97b27c1a5b1b825567850ee1c374 100644 --- a/app/src/main/res/layout/departure_bottom_sheet.xml +++ b/app/src/main/res/layout/departure_bottom_sheet.xml @@ -24,6 +24,7 @@ android:id="@+id/arrival_status" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="0dp" + android:visibility="gone" android:textAppearance="@style/TextAppearance.Material3.BodySmall" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -35,6 +36,7 @@ android:id="@+id/arrival_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="0dp" + android:visibility="gone" android:textAppearance="@style/TextAppearance.Material3.DisplaySmall" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/result.xml b/app/src/main/res/layout/result.xml index 76614ac6dfa60c71c84bfc0c97448af7c18426af..6146e5c5891d3f052e445ddd3a2e4c8fff9fb1a3 100644 --- a/app/src/main/res/layout/result.xml +++ b/app/src/main/res/layout/result.xml @@ -74,7 +74,7 @@ tool:text="TfL London" /> <!-- todo maxWidth or separate layout for graphView --> <com.google.android.material.textview.MaterialTextView - android:id="@+id/suggestion_description" + android:id="@+id/suggestion_change_options" style="@style/Theme.Bimba.SearchResult.Description" android:layout_width="0dp" android:layout_height="wrap_content" @@ -85,12 +85,31 @@ android:lineSpacingMultiplier="1.6" android:maxWidth="360dp" android:paddingTop="4dp" android:paddingBottom="4dp" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/distance" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="@+id/suggestion_title" app:layout_constraintTop_toBottomOf="@+id/feed_name" app:layout_constraintVertical_bias="1.0" tool:text="Metropolitan » Baker Street, Tower Hill The Monument, Westminster, Piccadilly Circus, Oxford Street" /> + + <!-- todo maxWidth or separate layout for graphView --> + <com.google.android.material.textview.MaterialTextView + android:id="@+id/suggestion_description" + style="@style/Theme.Bimba.SearchResult.Description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginEnd="4dp" + android:includeFontPadding="false" + android:lineSpacingMultiplier="1.6" + android:maxWidth="360dp" + android:paddingTop="4dp" + android:paddingBottom="4dp" + app:layout_constraintEnd_toStartOf="@+id/distance" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="@+id/suggestion_title" + app:layout_constraintTop_toBottomOf="@+id/suggestion_change_options" + app:layout_constraintVertical_bias="1.0" + tool:text="England, London" /> </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/menu/departures_menu.xml b/app/src/main/res/menu/departures_menu.xml index 9aef4cf6ed99cb06a663eeb16dedd94e4145a664..a5fd28a6a6ebcdcd9eca1d2b7da8a2d77228dedc 100644 --- a/app/src/main/res/menu/departures_menu.xml +++ b/app/src/main/res/menu/departures_menu.xml @@ -39,12 +39,10 @@ android:icon="@drawable/calendar" app:showAsAction="ifRoom" android:contentDescription="@string/title_select_date" android:title="@string/title_select_date"/> - <!-- TODO elizabeth <item android:id="@+id/terminus_arrival_showing" android:icon="@drawable/terminus" app:showAsAction="ifRoom" android:contentDescription="@string/terminus_arrival_showing" - android:title="@string/terminus_arrival_showing" - />--> + android:title="@string/terminus_arrival_showing" /> </menu> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d7eabb6448290335cfad6b1b52b8943562ede311..b00ba98ca26bb4cd3ff5911e98920bbbd4505df8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -273,7 +273,7 @@Bimba has crashed <string name="acra_notification_text">An unexpected obstruction has shown up on Bimba’s way. Do you want to send a report?</string> <string name="send">Send</string> <string name="discard">Discard</string> - <string name="send_with_comment">Send with commend</string> + <string name="send_with_comment">Send with comment</string> <string name="acra_notification_comment">Comment added to crash report</string> <string name="filtered_departures">Filtered departures</string> <string name="alerts">Alerts</string> @@ -286,6 +286,7 @@link to email <string name="transitous_description">A community-run provider-neutral international public transport routing service. Coverage is available at https://transitous.org/sources/</string> <string name="transitous_attribution">Transitous (https://transitous.org) API provided by Spline (https://routing.spline.de). Localities (https://github.com/public-transport/transitous/tree/main/feeds) maintained by the community.</string> <string name="local_time">local time</string> + <string name="arrow">Arrow pointing to the stop</string> <string name="departure_arrived">arrived</string> <string name="arrival_approximate">approx. arr.</string> <string name="arrival">arrival</string> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 1f990cc3b25e064ea38ef5e14bc97f87aa2297e5..7bc3779f9823af620c497c26f8a3c661eb9a8d37 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -78,4 +78,4 @@ <style name="Preference.SwitchPreferenceCompat" parent="@style/Preference.SwitchPreferenceCompat.Material" tool:ignore="ResourceCycle"> <item name="widgetLayout">@layout/preferences_switch_material</item> </style> -</resources> \ No newline at end of file +</resources> diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index bdc076567a58b017c5a152746e2ba04f6e14a2b6..febf014d61a368f90e36f1774a2b1fbcf8f3baff 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -271,7 +271,7 @@Bimba has crashed <string name="acra_notification_text">An unexpected obstruction has shown up on Bimba’s way. Do you want to send a report?</string> <string name="send">Send</string> <string name="discard">Discard</string> - <string name="send_with_comment">Send with commend</string> + <string name="send_with_comment">Send with comment</string> <string name="acra_notification_comment">Comment added to crash report</string> <string name="filtered_departures">Filtered departures</string> <string name="alerts">Alerts</string> @@ -284,6 +284,7 @@link to email <string name="transitous_description">A community-run provider-neutral international public transport routing service. Coverage is available at https://transitous.org/sources/</string> <string name="transitous_attribution">Transitous (https://transitous.org) API provided by Spline (https://routing.spline.de). Localities (https://github.com/public-transport/transitous/tree/main/feeds) maintained by the community.</string> <string name="local_time">local time</string> + <string name="arrow">Arrow pointing to the stop</string> <string name="departure_arrived">arrived</string> <string name="arrival_approximate">approx. arr.</string> <string name="arrival">arrival</string> diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml index 075852ab7b5dd2b4fc0fca4e326a543a1ea2f949..53b5db71611451c372af76e76bda67502c14d749 100644 --- a/app/src/main/res/values-en-rUS/strings.xml +++ b/app/src/main/res/values-en-rUS/strings.xml @@ -268,7 +268,7 @@Bimba crashed <string name="acra_notification_text">An unexpected obstruction showed up on Bimba’s way. Do you want to send a report?</string> <string name="send">Send</string> <string name="discard">Discard</string> - <string name="send_with_comment">Send with commend</string> + <string name="send_with_comment">Send with comment</string> <string name="acra_notification_comment">Comment added to crash report</string> <string name="filtered_departures">Filtered departures</string> <string name="alerts">Alerts</string> @@ -282,6 +282,7 @@No email app installed <string name="transitous_description">A community-run provider-neutral international public transport routing service. Coverage is available at https://transitous.org/sources/</string> <string name="transitous_attribution">Transitous (https://transitous.org) API provided by Spline (https://routing.spline.de). Localities (https://github.com/public-transport/transitous/tree/main/feeds) maintained by the community.</string> <string name="local_time">local time</string> + <string name="arrow">Arrow pointing to the stop</string> <string name="departure_arrived">arrived</string> <string name="arrival_approximate">approx. arr.</string> <string name="arrival">arrival</string> diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 9e162712580cf6ac01c38c1dec5c8a8337d52f9f..09dc3798658a04c334efe19c2f20fe8924a6a437 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -302,6 +302,7 @@link do e-maila <string name="no_email_app">Brak aplikacji e-mail</string> <string name="transitous_attribution">API Transitous (https://transitous.org) dostarczane przez Spline (https://routing.spline.de). Lokalizacje (https://github.com/public-transport/transitous/tree/main/feeds) utrzymywane przez społeczność.</string> <string name="local_time">czas lokalny</string> + <string name="arrow">strzałka wskazująca kierunek do przystanku</string> <string name="departure_arrived">przyjechał</string> <string name="arrival_approximate">przyb. przyj.</string> <string name="arrival">przyjazd</string> diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 47230fb1e059c9c12699e5d229b2b65e3182d971..c82baad406fe24af9a7df280f960432d107ec9ee 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -38,7 +38,6 @@ app:key="line_decoration" app:title="@string/line_decorations" app:useSimpleSummaryProvider="true" /> - <!-- TODO elizabeth <ListPreference app:defaultValue="grey_out" app:entries="@array/terminus_arrival_showing" @@ -47,6 +46,6 @@ app:icon="@drawable/terminus" app:key="terminus_arrival_showing" app:title="@string/terminus_arrival_showing" app:useSimpleSummaryProvider="true" - />--> + /> </PreferenceCategory> </PreferenceScreen> \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 86f0ddbf3403895e1c7ff8b91dd0849b8edda743..469ffc011b5a5a8007953b795633669f6779049e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id("com.android.application") version "8.7.2" apply false id("com.android.library") version "8.7.2" apply false + id("org.openapi.generator") version "7.9.0" apply false + id("de.undercouch.download") version "5.6.0" apply false kotlin("android") version "2.0.10" apply false kotlin("jvm") version "1.7.20" apply false kotlin("plugin.parcelize") version "1.8.20" apply false diff --git a/metadata/en/changelogs/34.txt b/metadata/en/changelogs/34.txt new file mode 100644 index 0000000000000000000000000000000000000000..2850a6537f1b4180d4c748184bddd828cfbb0898 --- /dev/null +++ b/metadata/en/changelogs/34.txt @@ -0,0 +1 @@ +* switch to MOTIS 2 for Transitous diff --git a/metadata/en-US/changelogs/34.txt b/metadata/en-US/changelogs/34.txt new file mode 100644 index 0000000000000000000000000000000000000000..2850a6537f1b4180d4c748184bddd828cfbb0898 --- /dev/null +++ b/metadata/en-US/changelogs/34.txt @@ -0,0 +1 @@ +* switch to MOTIS 2 for Transitous diff --git a/metadata/pl-PL/changelogs/34.txt b/metadata/pl-PL/changelogs/34.txt new file mode 100644 index 0000000000000000000000000000000000000000..da3c6ffd9c2a9460320ec9e6c68832f8b9da75ba --- /dev/null +++ b/metadata/pl-PL/changelogs/34.txt @@ -0,0 +1 @@ +* przełączenie na MOTIS 2 dla Transitous