Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ google_maps_api.xml
/build/intermediates/lint-cache/maven.google/com/google/.DS_Store
/app/src/main/res/values/.DS_Store
/build/intermediates/lint-cache/maven.google/com/google/android/.DS_Store
/build/reports/
7 changes: 4 additions & 3 deletions app/src/main/java/edu/rpi/shuttletracker/data/models/Route.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type

data class Route(
Expand Down Expand Up @@ -38,19 +39,19 @@ class RouteDeserializer : JsonDeserializer<Route> {
val stops: List<String> =
context.deserialize(
obj["STOPS"],
object : com.google.gson.reflect.TypeToken<List<String>>() {}.type,
object : TypeToken<List<String>>() {}.type,
)

val polylineStops: List<String> =
context.deserialize(
obj["POLYLINE_STOPS"],
object : com.google.gson.reflect.TypeToken<List<String>>() {}.type,
object : TypeToken<List<String>>() {}.type,
)

val coordinates: List<List<List<Double>>> =
context.deserialize(
obj["ROUTES"],
object : com.google.gson.reflect.TypeToken<List<List<List<Double>>>>() {}.type,
object : TypeToken<List<List<List<Double>>>>() {}.type,
)

// Decode dynamic stop keys, but only those listed in STOPS
Expand Down
110 changes: 110 additions & 0 deletions app/src/main/java/edu/rpi/shuttletracker/data/models/Vehicle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package edu.rpi.shuttletracker.data.models

import com.google.android.gms.maps.model.LatLng
import com.google.gson.annotations.SerializedName
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.time.Duration
import java.time.Instant
import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit
import java.util.Locale
import kotlin.String

data class Vehicle(
val id: String,
val name: String,
// from locations
val latitude: Double,
val longitude: Double,
val speedMph: Double,
val timestamp: String,
// from velocities
val routeName: String?,
val isAtStop: Boolean?,
val currentStop: String?,
// from etas
val stopTimes: Map<String, String>,
) {
/**
* Turns the date stored into a time of a generalized time ago from current
* updates once per second if subscribed to
* */
fun getTimeAgo(): Flow<String> {
val busInstant =
OffsetDateTime.parse(timestamp).toInstant()

return flow {
while (true) {
val now = Instant.now().truncatedTo(ChronoUnit.SECONDS)
val duration = Duration.between(busInstant, now)

emit(formatDuration(duration) + " ago")
delay(1000)
}
}
}

// Pretty "xh ym zs" formatter (avoids relying on Duration.toString())
private fun formatDuration(d: Duration): String {
var secs = d.seconds
val h = secs / 3600
secs %= 3600
val m = secs / 60
secs %= 60
val s = secs
return buildString {
if (h > 0) append("${h}h ")
if (m > 0 || h > 0) append("${m}m ")
append("${s}s")
}.trim().lowercase(Locale.ROOT)
}

fun latLng() = LatLng(latitude, longitude)
}

data class VehicleLocation(
val name: String,
val latitude: Double,
val longitude: Double,
@SerializedName("speed_mph") val speedMph: Double,
val timestamp: String,
)

data class VehicleStopEta(
@SerializedName("stop_times") val stopTimes: Map<String, String>,
@SerializedName("timestamp") val timestamp: String,
)

data class VehicleVelocities(
@SerializedName("route_name") val routeName: String,
@SerializedName("is_at_stop") val isAtStop: Boolean,
@SerializedName("current_stop") val currentStop: String?,
)

object VehicleMerger {
fun merge(
locations: Map<String, VehicleLocation>,
velocities: Map<String, VehicleVelocities> = emptyMap(),
etas: Map<String, VehicleStopEta> = emptyMap(),
): List<Vehicle> =
locations
.map { (vehicleId, location) ->
val velocity = velocities[vehicleId]
val eta = etas[vehicleId]

Vehicle(
id = vehicleId,
name = location.name,
latitude = location.latitude,
longitude = location.longitude,
speedMph = location.speedMph,
timestamp = location.timestamp,
routeName = velocity?.routeName,
isAtStop = velocity?.isAtStop,
currentStop = velocity?.currentStop,
stopTimes = eta?.stopTimes ?: emptyMap(),
)
}.sortedBy { it.name }
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import edu.rpi.shuttletracker.data.models.Announcement
import edu.rpi.shuttletracker.data.models.ErrorResponse
import edu.rpi.shuttletracker.data.models.Route
import edu.rpi.shuttletracker.data.models.Schedule
import edu.rpi.shuttletracker.data.models.vehicle.VehicleLocation
import edu.rpi.shuttletracker.data.models.vehicle.VehicleStopEta
import edu.rpi.shuttletracker.data.models.VehicleLocation
import edu.rpi.shuttletracker.data.models.VehicleStopEta
import edu.rpi.shuttletracker.data.models.VehicleVelocities

interface ApiHelper {
suspend fun getVehicleLocations(): NetworkResponse<Map<String, VehicleLocation>, ErrorResponse>

suspend fun getVehicleEtas(): NetworkResponse<Map<String, VehicleStopEta>, ErrorResponse>

suspend fun getVehicleVelocities(): NetworkResponse<Map<String, VehicleVelocities>, ErrorResponse>

suspend fun getRoutes(): NetworkResponse<Map<String, Route>, ErrorResponse>

suspend fun getAnnouncements(): NetworkResponse<List<Announcement>, ErrorResponse>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import edu.rpi.shuttletracker.data.models.Announcement
import edu.rpi.shuttletracker.data.models.ErrorResponse
import edu.rpi.shuttletracker.data.models.Route
import edu.rpi.shuttletracker.data.models.Schedule
import edu.rpi.shuttletracker.data.models.vehicle.VehicleLocation
import edu.rpi.shuttletracker.data.models.vehicle.VehicleStopEta
import edu.rpi.shuttletracker.data.models.VehicleLocation
import edu.rpi.shuttletracker.data.models.VehicleStopEta
import edu.rpi.shuttletracker.data.models.VehicleVelocities
import javax.inject.Inject

class ApiHelperImpl
Expand All @@ -22,6 +23,9 @@ class ApiHelperImpl
override suspend fun getVehicleEtas(): NetworkResponse<Map<String, VehicleStopEta>, ErrorResponse> =
apiService.getVehicleEtas()

override suspend fun getVehicleVelocities(): NetworkResponse<Map<String, VehicleVelocities>, ErrorResponse> =
apiService.getVehicleVelocities()

override suspend fun getRoutes(): NetworkResponse<Map<String, Route>, ErrorResponse> = apiService.getRoutes()

override suspend fun getAnnouncements(): NetworkResponse<List<Announcement>, ErrorResponse> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import edu.rpi.shuttletracker.data.models.Announcement
import edu.rpi.shuttletracker.data.models.ErrorResponse
import edu.rpi.shuttletracker.data.models.Route
import edu.rpi.shuttletracker.data.models.Schedule
import edu.rpi.shuttletracker.data.models.vehicle.VehicleLocation
import edu.rpi.shuttletracker.data.models.vehicle.VehicleStopEta
import edu.rpi.shuttletracker.data.models.VehicleLocation
import edu.rpi.shuttletracker.data.models.VehicleStopEta
import edu.rpi.shuttletracker.data.models.VehicleVelocities
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
Expand All @@ -20,6 +21,9 @@ interface ApiService {
@GET("etas")
suspend fun getVehicleEtas(): NetworkResponse<Map<String, VehicleStopEta>, ErrorResponse>

@GET("velocities")
suspend fun getVehicleVelocities(): NetworkResponse<Map<String, VehicleVelocities>, ErrorResponse>

@GET("routes")
suspend fun getRoutes(): NetworkResponse<Map<String, Route>, ErrorResponse>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ class ApiRepository
}
}

fun observeVehicleVelocities(pollMs: Long = 30_000L) =
flow {
while (currentCoroutineContext().isActive) {
emit(apiHelper.getVehicleVelocities())
delay(pollMs)
}
}

suspend fun getRoutes() = apiHelper.getRoutes()

suspend fun getAnnouncements() = apiHelper.getAnnouncements()
Expand Down
Loading
Loading