Author: Adam Evyčędo <git@apiote.xyz>
prepare motis 2 # Conflicts: # app/src/main/java/xyz/apiote/bimba/czwek/api/transitousDepartures.kt
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt index 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..fcf627e0871257a5435aafb88f8e5e2be49325f6 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,13 +22,12 @@ 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, @@ -41,247 +38,59 @@ 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 - } - } - """ - - 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) + 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())) } - } 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.map { + Log.i("stop", "stopID recvd: ${it.place.stopId}") + if (it.place.arrival == null) { + null + } else { + latitude = it.place.lat + longitude = it.place.lon + stopName = it.place.name + Departure( + it.tripId + it.source, + Time.fromOffsetTime(it.place.arrival, 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() - ) - } + }.filterNotNull() + Log.i("stop", "stopID asked: $stop") + StopDepartures( + departures, + Stop( + stop, + stopName, + stopName, + "", + "transitous", + Position(latitude.toDouble(), longitude.toDouble()), + listOf(), + null + ), + 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 - } - } - 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/departures/Departures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt index 01f592aa9c53a29de8ef478f6d1ff9f504d97335..fe2c6f61568b7c976c20ded71586077877d1329b 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 @@ -633,4 +633,4 @@ } } return content } -} \ 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 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/LineType.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/LineType.kt index 46be84493816f7bcf0e5e8d25416344dc81b6172..23b70c21de9ad5346c865729fafc721eac726b8c 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; @@ -59,6 +60,38 @@ 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.BIKE_RENTAL -> UNKNOWN + Mode.BIKE_TO_PARK -> UNKNOWN + Mode.CAR_TO_PARK -> UNKNOWN + Mode.CAR_HAILING -> UNKNOWN + Mode.CAR_SHARING -> UNKNOWN + Mode.CAR_PICKUP -> UNKNOWN + Mode.CAR_RENTAL -> UNKNOWN + Mode.FLEXIBLE -> UNKNOWN + Mode.SCOOTER_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 } } } 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..95819178a6e46c81785a0ba505ce4fab752ac98d 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 @@ -174,12 +173,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..040b148244d5331bbd508a160687ab3aa747894d 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 @@ -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..b8e446eebb0382c72d17ccc9290b5cf967fe8d37 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 @@ -114,20 +114,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 +147,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 +156,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) } } 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