diff --git a/.gitignore b/.gitignore index 83f099d..975a370 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,6 @@ captures **/xcshareddata/WorkspaceSettings.xcsettings secrets.properties -shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt +/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt /data/ /data diff --git a/build.gradle.kts b/build.gradle.kts index 0687328..9434477 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.composeCompiler) apply false alias(libs.plugins.kotlinJvm) apply false alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.sqldelight) apply false alias(libs.plugins.wire) apply false } diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/route/RouteLocalDataSource.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/route/RouteLocalDataSource.kt deleted file mode 100644 index e89d4e9..0000000 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/route/RouteLocalDataSource.kt +++ /dev/null @@ -1,11 +0,0 @@ -package moe.lava.banksia.client.data.route - -import moe.lava.banksia.model.Route -import moe.lava.banksia.room.dao.RouteDao -import moe.lava.banksia.room.entity.asEntity - -class RouteLocalDataSource(private val dao: RouteDao) { - suspend fun get(id: String) = dao.get(id) - suspend fun getAll() = dao.getAll() - suspend fun save(vararg routes: Route) = dao.insertOrReplaceAll(*routes.map { it.asEntity() }.toTypedArray()) -} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/route/RouteRemoteDataSource.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/route/RouteRemoteDataSource.kt deleted file mode 100644 index cbe9804..0000000 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/route/RouteRemoteDataSource.kt +++ /dev/null @@ -1,11 +0,0 @@ -package moe.lava.banksia.client.data.route - -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.request.get -import moe.lava.banksia.model.Route - -class RouteRemoteDataSource(val client: HttpClient) { - suspend fun get(id: String) = client.get("routes/${id}").body() - suspend fun getAll() = client.get("routes").body>() -} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stop/StopLocalDataSource.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stop/StopLocalDataSource.kt deleted file mode 100644 index 486aae0..0000000 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stop/StopLocalDataSource.kt +++ /dev/null @@ -1,12 +0,0 @@ -package moe.lava.banksia.client.data.stop - -import moe.lava.banksia.model.Stop -import moe.lava.banksia.room.dao.RouteDao -import moe.lava.banksia.room.dao.StopDao -import moe.lava.banksia.room.entity.asEntity - -class StopLocalDataSource(private val dao: StopDao, private val routeDao: RouteDao) { - suspend fun get(id: String) = dao.get(id) - suspend fun getByRoute(id: String) = routeDao.stops(id) - suspend fun save(vararg stops: Stop) = dao.insertOrReplaceAll(*stops.map { it.asEntity() }.toTypedArray()) -} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeLocalDataSource.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeLocalDataSource.kt deleted file mode 100644 index 3640b1f..0000000 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeLocalDataSource.kt +++ /dev/null @@ -1,28 +0,0 @@ -package moe.lava.banksia.client.data.stoptime - -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.todayIn -import moe.lava.banksia.model.StopTimeDated -import moe.lava.banksia.model.atDate -import moe.lava.banksia.room.dao.StopTimeDao -import moe.lava.banksia.util.serialise -import kotlin.time.Clock - -class StopTimeLocalDataSource( - private val stopTimeDao: StopTimeDao, -) { - suspend fun getAtStop( - stopId: String, - date: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), - ): List { - return stopTimeDao - .getForStopDated( - stopId, - listOf(date.dayOfWeek).serialise(), - date.toEpochDays().toInt(), - ) - .map { it.asModel().atDate(date) } - .sortedBy { it.departureTime } - } -} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeRemoteDataSource.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeRemoteDataSource.kt deleted file mode 100644 index baf26e7..0000000 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeRemoteDataSource.kt +++ /dev/null @@ -1,36 +0,0 @@ -package moe.lava.banksia.client.data.stoptime - -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.request.get -import io.ktor.client.request.parameter -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.todayIn -import moe.lava.banksia.model.StopTimeDated -import kotlin.time.Clock - -class StopTimeRemoteDataSource( - private val client: HttpClient, -) { - suspend fun getAtStop( - stopId: String, - date: LocalDate? = Clock.System.todayIn(TimeZone.currentSystemDefault()), - ): List { - return client.get("stoptimes/by_stop/${stopId}") { - parameter("date", date) - }.body>() - } - - /*suspend fun get( - stop: String? = null, - trip: String? = null, - day: DayOfWeek? = Clock.System.todayIn(TimeZone.currentSystemDefault()).dayOfWeek, - ): List { - return client.get("stoptimes") { - stop?.let { parameter("stop", it) } - trip?.let { parameter("trip", it) } - day?.let { parameter("day", it) } - }.body>() - }*/ -} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/trip/TripRemoteDataSource.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/trip/TripRemoteDataSource.kt deleted file mode 100644 index 8b46fbd..0000000 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/trip/TripRemoteDataSource.kt +++ /dev/null @@ -1,18 +0,0 @@ -package moe.lava.banksia.client.data.trip - -import io.ktor.client.HttpClient -import kotlinx.datetime.DayOfWeek -import kotlinx.datetime.TimeZone -import kotlinx.datetime.todayIn -import moe.lava.banksia.model.Trip -import kotlin.time.Clock - -class TripRemoteDataSource( - private val client: HttpClient, -) { - suspend fun get( - day: DayOfWeek? = Clock.System.todayIn(TimeZone.currentSystemDefault()).dayOfWeek, - ): List { - return listOf() - } -} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/RouteRepository.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/RouteRepository.kt deleted file mode 100644 index 22a6bcc..0000000 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/RouteRepository.kt +++ /dev/null @@ -1,25 +0,0 @@ -package moe.lava.banksia.client.repository - -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import moe.lava.banksia.client.data.route.RouteLocalDataSource -import moe.lava.banksia.client.data.route.RouteRemoteDataSource - -class RouteRepository( - private val local: RouteLocalDataSource, - private val remote: RouteRemoteDataSource, -) { - private val mutex = Mutex() - suspend fun getAll() = mutex.withLock { - local - .getAll() - .map { it.asModel() } - .ifEmpty { - remote - .getAll() - .also { local.save(*it.toTypedArray()) } - } - } - - suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) } -} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopRepository.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopRepository.kt deleted file mode 100644 index 690616a..0000000 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopRepository.kt +++ /dev/null @@ -1,22 +0,0 @@ -package moe.lava.banksia.client.repository - -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import moe.lava.banksia.client.data.stop.StopLocalDataSource -import moe.lava.banksia.client.data.stop.StopRemoteDataSource - -class StopRepository( - private val local: StopLocalDataSource, - private val remote: StopRemoteDataSource, -) { - private val mutex = Mutex() - - suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) } - suspend fun getByRoute(id: String) = mutex.withLock { - local - .getByRoute(id) - .map { it.asModel() } - .ifEmpty { null } - ?: remote.getByRoute(id) - } -} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopTimeRepository.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopTimeRepository.kt deleted file mode 100644 index 4f54840..0000000 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopTimeRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package moe.lava.banksia.client.repository - -import moe.lava.banksia.client.data.stoptime.StopTimeLocalDataSource -import moe.lava.banksia.client.data.stoptime.StopTimeRemoteDataSource -import moe.lava.banksia.model.StopTimeDated - -class StopTimeRepository( - private val local: StopTimeLocalDataSource, - private val remote: StopTimeRemoteDataSource, -) { - suspend fun getForStop(id: String): List { - return local - .getAtStop(id) - .ifEmpty { remote.getAtStop(id) } - } -} diff --git a/shared/build.gradle.kts b/core/build.gradle.kts similarity index 70% rename from shared/build.gradle.kts rename to core/build.gradle.kts index 953d790..3dd2ee6 100644 --- a/shared/build.gradle.kts +++ b/core/build.gradle.kts @@ -4,18 +4,11 @@ plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinSerialization) alias(libs.plugins.androidMultiplatformLibrary) - alias(libs.plugins.ksp) - alias(libs.plugins.room) - alias(libs.plugins.wire) -} - -room { - schemaDirectory("$projectDir/schemas") } kotlin { android { - namespace = "moe.lava.banksia.shared" + namespace = "moe.lava.banksia.core" compileSdk = libs.versions.android.compileSdk.get().toInt() compilerOptions { @@ -47,25 +40,9 @@ kotlin { implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.protobuf) - implementation(libs.room.runtime) - implementation(libs.sqlite.bundled) } iosMain.dependencies { implementation(libs.ktor.client.darwin) } } } - -dependencies { - add("kspAndroid", libs.room.compiler) - add("kspIosArm64", libs.room.compiler) - add("kspIosSimulatorArm64", libs.room.compiler) - add("kspJvm", libs.room.compiler) -} - -wire { - sourcePath { - srcDir("src/commonMain/proto") - } - kotlin {} -} diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts new file mode 100644 index 0000000..ecdba19 --- /dev/null +++ b/core/data/build.gradle.kts @@ -0,0 +1,64 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidMultiplatformLibrary) +} + +kotlin { + android { + namespace = "moe.lava.banksia.core.data" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + } + + iosArm64() + iosSimulatorArm64() + + jvm() + + sourceSets { + val clientMain by creating { + dependsOn(commonMain.get()) + } + + androidMain.get().dependsOn(clientMain) + iosArm64Main.get().dependsOn(clientMain) + iosSimulatorArm64Main.get().dependsOn(clientMain) + + commonMain.dependencies { + implementation(libs.koin.core) + implementation(projects.core) + api(projects.core.stoptime) + } + + androidMain.dependencies { + implementation(libs.koin.compose) + implementation(libs.ktor.client.okhttp) + } + commonMain.dependencies { + implementation(libs.okio) + implementation(libs.koin.core) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.contentnegotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.serialization.protobuf) + + implementation(projects.core) + implementation(projects.core.sqld) + } + iosMain.dependencies { + implementation(libs.ktor.client.darwin) + } + } +} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/DataDiModule.client.kt similarity index 57% rename from client/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt rename to core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/DataDiModule.client.kt index f22c7db..104c6bc 100644 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt +++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/DataDiModule.client.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.client.di +package moe.lava.banksia.core.data import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpSend @@ -7,22 +7,22 @@ import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.plugin import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -import moe.lava.banksia.Constants -import moe.lava.banksia.client.data.route.RouteLocalDataSource -import moe.lava.banksia.client.data.route.RouteRemoteDataSource -import moe.lava.banksia.client.data.stop.StopLocalDataSource -import moe.lava.banksia.client.data.stop.StopRemoteDataSource -import moe.lava.banksia.client.data.stoptime.StopTimeLocalDataSource -import moe.lava.banksia.client.data.stoptime.StopTimeRemoteDataSource -import moe.lava.banksia.client.repository.RouteRepository -import moe.lava.banksia.client.repository.StopRepository -import moe.lava.banksia.client.repository.StopTimeRepository +import moe.lava.banksia.core.Constants +import moe.lava.banksia.core.data.repositories.ClientRouteRepository +import moe.lava.banksia.core.data.repositories.ClientStopRepository +import moe.lava.banksia.core.data.repositories.RouteRepository +import moe.lava.banksia.core.data.repositories.StopRepository +import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource +import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource +import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource +import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource +import moe.lava.banksia.core.util.log import moe.lava.banksia.data.ptv.PtvService -import moe.lava.banksia.util.log import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind import org.koin.dsl.module -val ClientModule = module { +actual val platformModule = module { // HTTP Clients singleOf(::PtvService) single { @@ -49,11 +49,8 @@ val ClientModule = module { singleOf(::RouteRemoteDataSource) singleOf(::StopLocalDataSource) singleOf(::StopRemoteDataSource) - singleOf(::StopTimeLocalDataSource) - singleOf(::StopTimeRemoteDataSource) // Repositories - singleOf(::RouteRepository) - singleOf(::StopRepository) - singleOf(::StopTimeRepository) + singleOf(::ClientRouteRepository) bind RouteRepository::class + singleOf(::ClientStopRepository) bind StopRepository::class } diff --git a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientRouteRepository.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientRouteRepository.kt new file mode 100644 index 0000000..f46caac --- /dev/null +++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientRouteRepository.kt @@ -0,0 +1,36 @@ +package moe.lava.banksia.core.data.repositories + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource +import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource +import moe.lava.banksia.core.model.Route +import moe.lava.banksia.core.sqld.mappers.asModel + +internal class ClientRouteRepository internal constructor( + private val local: RouteLocalDataSource, + private val remote: RouteRemoteDataSource, +) : RouteRepository { + private val mutex = Mutex() + override suspend fun getAll() = mutex.withLock { + local + .getAll() + .map { it.asModel() } + .ifEmpty { + remote + .getAll() + .also { local.save(*it.toTypedArray()) } + } + } + + private val tripRouteMap = mutableMapOf() + + override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) } + override suspend fun getByPattern(patternId: Long) = mutex.withLock { + tripRouteMap[patternId] + ?: remote.getByPattern(patternId).also { + local.save(it) + tripRouteMap[patternId] = it + } + } +} diff --git a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopRepository.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopRepository.kt new file mode 100644 index 0000000..0aee84e --- /dev/null +++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopRepository.kt @@ -0,0 +1,23 @@ +package moe.lava.banksia.core.data.repositories + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource +import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource +import moe.lava.banksia.core.sqld.mappers.asModel + +internal class ClientStopRepository internal constructor( + private val local: StopLocalDataSource, + private val remote: StopRemoteDataSource, +) : StopRepository { + private val mutex = Mutex() + + override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) } + override suspend fun getByRoute(id: String) = mutex.withLock { + local + .getByRoute(id) + .map { it.asModel() } + .ifEmpty { null } + ?: remote.getByRoute(id) + } +} diff --git a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteLocalDataSource.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteLocalDataSource.kt new file mode 100644 index 0000000..8286b1f --- /dev/null +++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteLocalDataSource.kt @@ -0,0 +1,23 @@ +package moe.lava.banksia.core.data.sources.route + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.withContext +import moe.lava.banksia.core.model.Route +import moe.lava.banksia.core.sqld.RouteQueries +import moe.lava.banksia.core.sqld.mappers.asDb + +internal class RouteLocalDataSource(private val queries: RouteQueries) { + suspend fun get(id: String) = withContext(Dispatchers.IO) { queries.get(id).executeAsOneOrNull() } + suspend fun getAll() = withContext(Dispatchers.IO) { queries.getAll().executeAsList() } +// suspend fun getByTrip(tripId: String) = dao.getByTrip(tripId) + suspend fun save(vararg routes: Route) { + withContext(Dispatchers.IO) { + queries.transaction { + routes.forEach { + queries.insert(it.asDb()) + } + } + } + } +} diff --git a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteRemoteDataSource.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteRemoteDataSource.kt new file mode 100644 index 0000000..15088fb --- /dev/null +++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteRemoteDataSource.kt @@ -0,0 +1,12 @@ +package moe.lava.banksia.core.data.sources.route + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import moe.lava.banksia.core.model.Route + +internal class RouteRemoteDataSource(val client: HttpClient) { + suspend fun get(id: String) = client.get("routes/${id}").body() + suspend fun getByPattern(patternId: Long) = client.get("routes/by_pattern/${patternId}").body() + suspend fun getAll() = client.get("routes").body>() +} diff --git a/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopLocalDataSource.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopLocalDataSource.kt new file mode 100644 index 0000000..524d123 --- /dev/null +++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopLocalDataSource.kt @@ -0,0 +1,22 @@ +package moe.lava.banksia.core.data.sources.stop + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.withContext +import moe.lava.banksia.core.model.Stop +import moe.lava.banksia.core.sqld.StopQueries +import moe.lava.banksia.core.sqld.mappers.asDb + +internal class StopLocalDataSource(private val queries: StopQueries) { + suspend fun get(id: String) = withContext(Dispatchers.IO) { queries.get(id).executeAsOneOrNull() } + suspend fun getByRoute(id: String) = withContext(Dispatchers.IO) { queries.getByRoute(id).executeAsList() } + suspend fun save(vararg stops: Stop) { + withContext(Dispatchers.IO) { + queries.transaction { + stops.forEach { + queries.insert(it.asDb()) + } + } + } + } +} diff --git a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stop/StopRemoteDataSource.kt b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt similarity index 64% rename from client/src/commonMain/kotlin/moe/lava/banksia/client/data/stop/StopRemoteDataSource.kt rename to core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt index 47c2f80..f39afd3 100644 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stop/StopRemoteDataSource.kt +++ b/core/data/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt @@ -1,11 +1,11 @@ -package moe.lava.banksia.client.data.stop +package moe.lava.banksia.core.data.sources.stop import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get -import moe.lava.banksia.model.Stop +import moe.lava.banksia.core.model.Stop -class StopRemoteDataSource(val client: HttpClient) { +internal class StopRemoteDataSource(val client: HttpClient) { suspend fun get(id: String) = client.get("stops/${id}").body() suspend fun getByRoute(id: String) = client.get("route_stops/${id}").body>() } diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/DataDiModule.kt b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/DataDiModule.kt new file mode 100644 index 0000000..eea6a0e --- /dev/null +++ b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/DataDiModule.kt @@ -0,0 +1,13 @@ +package moe.lava.banksia.core.data + +import moe.lava.banksia.core.sqld.sqldDiModule +import org.koin.core.module.Module +import org.koin.dsl.module + +internal expect val platformModule: Module + +val dataDiModule = module { + includes(platformModule) + includes(sqldDiModule) + includes(stopTimeDataDiModule) +} diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/RouteRepository.kt b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/RouteRepository.kt new file mode 100644 index 0000000..ef3d6f1 --- /dev/null +++ b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/RouteRepository.kt @@ -0,0 +1,9 @@ +package moe.lava.banksia.core.data.repositories + +import moe.lava.banksia.core.model.Route + +interface RouteRepository { + suspend fun get(id: String): Route? + suspend fun getByPattern(patternId: Long): Route? + suspend fun getAll(): List +} diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopRepository.kt b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopRepository.kt new file mode 100644 index 0000000..c663f89 --- /dev/null +++ b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopRepository.kt @@ -0,0 +1,8 @@ +package moe.lava.banksia.core.data.repositories + +import moe.lava.banksia.core.model.Stop + +interface StopRepository { + suspend fun get(id: String): Stop + suspend fun getByRoute(id: String): List +} diff --git a/core/data/src/jvmMain/kotlin/moe/lava/banksia/core/data/DataDiModule.jvm.kt b/core/data/src/jvmMain/kotlin/moe/lava/banksia/core/data/DataDiModule.jvm.kt new file mode 100644 index 0000000..78a44d1 --- /dev/null +++ b/core/data/src/jvmMain/kotlin/moe/lava/banksia/core/data/DataDiModule.jvm.kt @@ -0,0 +1,7 @@ +package moe.lava.banksia.core.data + +import org.koin.dsl.module + +internal actual val platformModule = module { + +} diff --git a/client/build.gradle.kts b/core/sqld/build.gradle.kts similarity index 51% rename from client/build.gradle.kts rename to core/sqld/build.gradle.kts index 29dbc08..472a908 100644 --- a/client/build.gradle.kts +++ b/core/sqld/build.gradle.kts @@ -4,11 +4,12 @@ plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinSerialization) alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.sqldelight) } kotlin { android { - namespace = "moe.lava.banksia.client" + namespace = "moe.lava.banksia.core.sqld" compileSdk = libs.versions.android.compileSdk.get().toInt() compilerOptions { @@ -16,28 +17,37 @@ kotlin { } } - compilerOptions { - freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") - } - iosArm64() iosSimulatorArm64() + jvm() + sourceSets { androidMain.dependencies { - implementation(libs.compose.ui.tooling.preview) - implementation(libs.androidx.activity.compose) - implementation(libs.kotlinx.coroutines.android) - implementation(libs.play.services.location) + implementation(libs.sqldelight.driver.android) } commonMain.dependencies { + implementation(libs.okio) implementation(libs.koin.core) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.datetime) - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.contentnegotiation) - implementation(libs.ktor.serialization.kotlinx.json) - implementation(projects.shared) + + implementation(projects.core) + } + nativeMain.dependencies { + implementation(libs.sqldelight.driver.native) + } + jvmMain.dependencies { + implementation(libs.sqldelight.driver.jvm) + } + } +} + +sqldelight { + databases { + register("BanksiaDatabase") { + packageName.set("moe.lava.banksia.core.sqld") + schemaOutputDirectory.set(file("src/commonMain/sqldelight/schema")) } } } diff --git a/core/sqld/src/androidMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.android.kt b/core/sqld/src/androidMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.android.kt new file mode 100644 index 0000000..c47613c --- /dev/null +++ b/core/sqld/src/androidMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.android.kt @@ -0,0 +1,14 @@ +package moe.lava.banksia.core.sqld + +import android.content.Context +import app.cash.sqldelight.driver.android.AndroidSqliteDriver +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +actual class DatabaseManager : KoinComponent { + actual val database by lazy { + val ctx = get().applicationContext + val driver = AndroidSqliteDriver(BanksiaDatabase.Schema, ctx, "${DBNAME}.db") + BanksiaDatabase(driver) + } +} diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.kt new file mode 100644 index 0000000..983eb58 --- /dev/null +++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.kt @@ -0,0 +1,7 @@ +package moe.lava.banksia.core.sqld + +internal const val DBNAME = "timetable" + +expect class DatabaseManager() { + val database: BanksiaDatabase +} diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/SqldDiModule.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/SqldDiModule.kt new file mode 100644 index 0000000..deee453 --- /dev/null +++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/SqldDiModule.kt @@ -0,0 +1,17 @@ +package moe.lava.banksia.core.sqld + +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val sqldDiModule = module { + singleOf(::DatabaseManager) + factory { get().database } + factory { get().routeQueries } + factory { get().serviceQueries } + factory { get().serviceExceptionQueries } + factory { get().shapeQueries } + factory { get().stopQueries } + factory { get().stoppingPatternQueries } + factory { get().stopTimeQueries } + factory { get().tripQueries } +} diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Route.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Route.kt new file mode 100644 index 0000000..f3a5521 --- /dev/null +++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Route.kt @@ -0,0 +1,14 @@ +package moe.lava.banksia.core.sqld.mappers + +import moe.lava.banksia.core.model.Route +import moe.lava.banksia.core.model.RouteType +import moe.lava.banksia.core.sqld.Route as DbRoute + +fun DbRoute.asModel() = Route( + id = id, + type = RouteType.from(type.toInt()), + number = number, + name = name, +) + +fun Route.asDb() = DbRoute(id, type.value.toLong(), number, name) diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Service.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Service.kt new file mode 100644 index 0000000..dbda5ea --- /dev/null +++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Service.kt @@ -0,0 +1,21 @@ +package moe.lava.banksia.core.sqld.mappers + +import kotlinx.datetime.LocalDate +import moe.lava.banksia.core.model.Service +import moe.lava.banksia.core.util.deserialiseDaysBitflag +import moe.lava.banksia.core.util.serialise +import moe.lava.banksia.core.sqld.Service as DbService + +fun DbService.asModel() = Service( + id = id, + days = days.toInt().deserialiseDaysBitflag(), + start = LocalDate.fromEpochDays(start), + end = LocalDate.fromEpochDays(end), +) + +fun Service.asDb() = DbService( + id = id, + days = days.serialise().toLong(), + start = start.toEpochDays(), + end = end.toEpochDays(), +) diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/ServiceException.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/ServiceException.kt new file mode 100644 index 0000000..ef0d201 --- /dev/null +++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/ServiceException.kt @@ -0,0 +1,17 @@ +package moe.lava.banksia.core.sqld.mappers + +import kotlinx.datetime.LocalDate +import moe.lava.banksia.core.model.ServiceException +import moe.lava.banksia.core.sqld.ServiceException as DbServiceException + +fun DbServiceException.asModel() = ServiceException( + serviceId = serviceId, + date = LocalDate.fromEpochDays(date), + type = type.toInt(), +) + +fun ServiceException.asDb() = DbServiceException( + serviceId = serviceId, + type = date.toEpochDays(), + date = type.toLong(), +) diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Shape.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Shape.kt new file mode 100644 index 0000000..4a8d7db --- /dev/null +++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Shape.kt @@ -0,0 +1,52 @@ +package moe.lava.banksia.core.sqld.mappers + +import moe.lava.banksia.core.model.Shape +import moe.lava.banksia.core.model.ShapePath +import moe.lava.banksia.core.util.Point +import moe.lava.banksia.core.sqld.Shape as DbShape + +fun DbShape.asModel() = Shape( + id = id, + path = bytesToPath(path), +) + +fun Shape.asDb() = DbShape( + id = id, + path = bytesFromPath(path), +) + +private fun bytesToPath(value: ByteArray): ShapePath { + return value + .asSequence() + .asIterable() + .chunked(8) { + (it[0].toLong() and 0xFF) or + (it[1].toLong() and 0xFF shl 8) or + (it[2].toLong() and 0xFF shl 16) or + (it[3].toLong() and 0xFF shl 24) or + (it[4].toLong() and 0xFF shl 32) or + (it[5].toLong() and 0xFF shl 40) or + (it[6].toLong() and 0xFF shl 48) or + (it[7].toLong() and 0xFF shl 56) + } + .map { Double.fromBits(it) } + .chunked(2) + .map { (lat, lng) -> Point(lat, lng) } + .toList() +} + +private fun bytesFromPath(path: ShapePath): ByteArray { + return path + .flatMap { (lat, lng) -> listOf(lat.toBits(), lng.toBits()) } + .flatMap { i -> listOf( + i.toByte(), + (i shr 8).toByte(), + (i shr 16).toByte(), + (i shr 24).toByte(), + (i shr 32).toByte(), + (i shr 40).toByte(), + (i shr 48).toByte(), + (i shr 56).toByte(), + ) } + .toByteArray() +} diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Stop.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Stop.kt new file mode 100644 index 0000000..3bf6b54 --- /dev/null +++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Stop.kt @@ -0,0 +1,26 @@ +package moe.lava.banksia.core.sqld.mappers + +import moe.lava.banksia.core.model.Stop +import moe.lava.banksia.core.util.Point +import moe.lava.banksia.core.sqld.Stop as DbStop + +fun DbStop.asModel() = Stop( + id = id, + name = name, + pos = Point(lat, lng), + parent = parent, + hasWheelChairBoarding = hasWheelChairBoarding == 1L, + level = level, + platformCode = platformCode, +) + +fun Stop.asDb() = DbStop( + id = id, + name = name, + lat = pos.lat, + lng = pos.lng, + parent = parent, + hasWheelChairBoarding = if (hasWheelChairBoarding) 1L else 0L, + level = level, + platformCode = platformCode +) diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StopTime.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StopTime.kt new file mode 100644 index 0000000..26d5390 --- /dev/null +++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StopTime.kt @@ -0,0 +1,27 @@ +package moe.lava.banksia.core.sqld.mappers + +import moe.lava.banksia.core.model.FutureTime +import moe.lava.banksia.core.model.FutureTime.Companion.asInt +import moe.lava.banksia.core.model.StopTime +import moe.lava.banksia.core.model.TimeType +import moe.lava.banksia.core.sqld.StopTime as DbStopTime + +fun DbStopTime.asModel() = StopTime( + patternId = patternId, + stopId = stopId, + time = TimeType.Undated( + arrival = FutureTime.fromInt((departureTime + arrivalDelta).toInt()), + departure = FutureTime.fromInt(departureTime.toInt()), + ), + pickupType = pickupType.toInt(), + dropOffType = dropOffType.toInt(), +) + +fun StopTime.Undated.asDb() = DbStopTime( + patternId = patternId, + stopId = stopId, + arrivalDelta = (time.arrival.asInt() - time.departure.asInt()).toLong(), + departureTime = time.departure.asInt().toLong(), + pickupType = pickupType.toLong(), + dropOffType = dropOffType.toLong(), +) diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StoppingPattern.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StoppingPattern.kt new file mode 100644 index 0000000..d1409a2 --- /dev/null +++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/StoppingPattern.kt @@ -0,0 +1,23 @@ +package moe.lava.banksia.core.sqld.mappers + +import moe.lava.banksia.core.model.StopTime +import moe.lava.banksia.core.model.StoppingPattern +import moe.lava.banksia.core.model.TimeType +import moe.lava.banksia.core.sqld.StoppingPattern as DbStoppingPattern + +fun DbStoppingPattern.asModel(stoptimes: List>) = StoppingPattern( + id = id, + routeId = routeId, + shapeId = shapeId, + headsign = headsign, + wheelchairAccessible = wheelchairAccessible == 1L, + stoptimes = stoptimes, +) + +fun StoppingPattern<*>.asDb() = DbStoppingPattern( + id = id, + routeId = routeId, + shapeId = shapeId, + headsign = headsign, + wheelchairAccessible = if (wheelchairAccessible) 1L else 0L, +) diff --git a/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Trip.kt b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Trip.kt new file mode 100644 index 0000000..b3443fb --- /dev/null +++ b/core/sqld/src/commonMain/kotlin/moe/lava/banksia/core/sqld/mappers/Trip.kt @@ -0,0 +1,27 @@ +package moe.lava.banksia.core.sqld.mappers + +import moe.lava.banksia.core.model.Service +import moe.lava.banksia.core.model.StoppingPattern +import moe.lava.banksia.core.model.Trip +import moe.lava.banksia.core.sqld.Trip as DbTrip + +fun DbTrip.asModel(pattern: StoppingPattern.Undated, service: Service): Trip.Undated { + if (serviceId != service.id) { + throw IllegalArgumentException("trip and service id mismatch (${serviceId} != ${service.id})") + } + return Trip( + id = gtfsId, + pattern = pattern, + service = service, + directionId = directionId.toInt(), + blockId = blockId.toString(), + ) +} + +fun Trip.Undated.asDb() = DbTrip( + gtfsId = id, + patternId = pattern.id, + serviceId = service.id, + directionId = directionId.toLong(), + blockId = blockId?.toLong(), +) diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Route.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Route.sq new file mode 100644 index 0000000..e607975 --- /dev/null +++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Route.sq @@ -0,0 +1,20 @@ +CREATE TABLE Route ( + id TEXT PRIMARY KEY NOT NULL, + type INTEGER NOT NULL, + number TEXT, + name TEXT NOT NULL +); + +getAll: +SELECT * FROM Route; + +get: +SELECT * FROM Route WHERE id == ?; + +getByPattern: +SELECT Route.* FROM Route +INNER JOIN StoppingPattern ON Route.id == StoppingPattern.routeId +WHERE StoppingPattern.id == :patternId; + +insert: +INSERT OR REPLACE INTO Route VALUES ?; diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Service.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Service.sq new file mode 100644 index 0000000..a1c5fad --- /dev/null +++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Service.sq @@ -0,0 +1,11 @@ +CREATE TABLE Service ( + id TEXT PRIMARY KEY NOT NULL, + days INTEGER NOT NULL, + start INTEGER NOT NULL, + end INTEGER NOT NULL +); + +CREATE INDEX idx_Service_days ON Service (days); + +insert: +INSERT INTO Service VALUES ?; diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/ServiceException.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/ServiceException.sq new file mode 100644 index 0000000..332f198 --- /dev/null +++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/ServiceException.sq @@ -0,0 +1,9 @@ +CREATE TABLE ServiceException ( + serviceId TEXT NOT NULL, + type INTEGER NOT NULL, + date INTEGER NOT NULL, + PRIMARY KEY (serviceId, type) +); + +insert: +INSERT INTO ServiceException VALUES ?; diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Shape.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Shape.sq new file mode 100644 index 0000000..8734200 --- /dev/null +++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Shape.sq @@ -0,0 +1,7 @@ +CREATE TABLE Shape ( + id TEXT PRIMARY KEY NOT NULL, + path BLOB NOT NULL +); + +insert: +INSERT INTO Shape VALUES ?; diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Stop.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Stop.sq new file mode 100644 index 0000000..4af5c50 --- /dev/null +++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Stop.sq @@ -0,0 +1,54 @@ +CREATE TABLE Stop ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + lat REAL NOT NULL, + lng REAL NOT NULL, + parent TEXT REFERENCES Stop(id), + hasWheelChairBoarding INTEGER NOT NULL, + level TEXT, + platformCode TEXT +); + +CREATE INDEX idx_Stop_parent ON Stop (parent); + +getAll: +SELECT * FROM Stop; + +getAllParentless: +SELECT * FROM Stop WHERE platformCode IS NOT NULL AND parent IS NULL; + +get: +SELECT * FROM Stop WHERE id == ?; + +getMany: +SELECT * FROM Stop WHERE id IN ?; + +insert: +INSERT INTO Stop VALUES ?; + +updateParents: +UPDATE Stop SET parent = ? WHERE id IN ?; + +getByRoute: +SELECT Stop.* FROM Stop +INNER JOIN StopTime ON StopTime.stopId == Stop.id +INNER JOIN StoppingPattern ON StoppingPattern.id == StopTime.patternId +WHERE StoppingPattern.routeId == :id +GROUP BY Stop.id; + +-- I vibecoded this, sorry +getParentsByRoute: +WITH RECURSIVE Tree AS ( + SELECT Stop.* FROM Stop + INNER JOIN StopTime ON StopTime.stopId == Stop.id + INNER JOIN StoppingPattern ON StoppingPattern.id == StopTime.patternId + WHERE StoppingPattern.routeId == :id + GROUP BY Stop.id + + UNION ALL + + SELECT s.* + FROM Stop s + INNER JOIN Tree t ON s.id = t.parent +) +SELECT DISTINCT * FROM Tree WHERE parent IS NULL; diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StopTime.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StopTime.sq new file mode 100644 index 0000000..06bd76b --- /dev/null +++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StopTime.sq @@ -0,0 +1,45 @@ +CREATE TABLE StopTime ( + patternId INTEGER NOT NULL REFERENCES StoppingPattern (id), + stopId TEXT NOT NULL REFERENCES Stop (id), + arrivalDelta INTEGER NOT NULL, + departureTime INTEGER NOT NULL, + pickupType INTEGER NOT NULL, + dropOffType INTEGER NOT NULL, + PRIMARY KEY (patternId, stopId) +) WITHOUT ROWID; + +CREATE INDEX idx_StopTime_stopId ON StopTime (stopId); + +insert: +INSERT OR REPLACE INTO StopTime VALUES ?; + +getForStopDated: +SELECT DISTINCT StopTime.* FROM StopTime +INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end` +LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date +INNER JOIN Trip ON Trip.serviceId == Service.id +INNER JOIN StoppingPattern ON StoppingPattern.id == Trip.patternId +WHERE StopTime.patternId == StoppingPattern.id + AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId) + AND ServiceException.type IS NULL; + +getExtendedForStop: +SELECT DISTINCT + StopTime.patternId, + StopTime.arrivalDelta, + StopTime.departureTime, + StoppingPattern.headsign, + Route.type AS routeType, + Route.number AS routeNumber, + Route.name AS routeName, + Stop.platformCode AS stopPlatformCode +FROM StopTime +INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end` +LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date +INNER JOIN Trip ON Trip.serviceId == Service.id +INNER JOIN StoppingPattern ON StoppingPattern.id == Trip.patternId +INNER JOIN Route ON Route.id == StoppingPattern.routeId +INNER JOIN Stop ON Stop.id == StopTime.stopId +WHERE StopTime.patternId == StoppingPattern.id + AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId) + AND ServiceException.type IS NULL; diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StoppingPattern.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StoppingPattern.sq new file mode 100644 index 0000000..9a09e69 --- /dev/null +++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/StoppingPattern.sq @@ -0,0 +1,13 @@ +CREATE TABLE StoppingPattern ( + id INTEGER PRIMARY KEY NOT NULL, + routeId TEXT NOT NULL REFERENCES Route (id), + shapeId TEXT NOT NULL REFERENCES Shape (id), + headsign TEXT NOT NULL, + wheelchairAccessible INTEGER NOT NULL +); + +insert: +INSERT OR REPLACE INTO StoppingPattern VALUES ?; + +get: +SELECT * FROM StoppingPattern WHERE id == :id; diff --git a/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Trip.sq b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Trip.sq new file mode 100644 index 0000000..c53b62a --- /dev/null +++ b/core/sqld/src/commonMain/sqldelight/moe/lava/banksia/core/sqld/Trip.sq @@ -0,0 +1,13 @@ +CREATE TABLE Trip ( + gtfsId TEXT PRIMARY KEY NOT NULL, + patternId INTEGER NOT NULL REFERENCES StoppingPattern (id), + serviceId TEXT NOT NULL REFERENCES Service (id), + blockId INTEGER, + directionId INTEGER NOT NULL +); + +CREATE INDEX idx_Trip_patternId ON Trip (patternId); +CREATE INDEX idx_Trip_serviceId ON Trip (serviceId); + +insert: +INSERT OR REPLACE INTO Trip VALUES ?; diff --git a/core/sqld/src/commonMain/sqldelight/schema/1.db b/core/sqld/src/commonMain/sqldelight/schema/1.db new file mode 100644 index 0000000..feaacb3 Binary files /dev/null and b/core/sqld/src/commonMain/sqldelight/schema/1.db differ diff --git a/core/sqld/src/iosMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.ios.kt b/core/sqld/src/iosMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.ios.kt new file mode 100644 index 0000000..9ce0627 --- /dev/null +++ b/core/sqld/src/iosMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.ios.kt @@ -0,0 +1,11 @@ +package moe.lava.banksia.core.sqld + +import app.cash.sqldelight.driver.native.NativeSqliteDriver +import org.koin.core.component.KoinComponent + +actual class DatabaseManager : KoinComponent { + actual val database by lazy { + val driver = NativeSqliteDriver(BanksiaDatabase.Schema, "${DBNAME}.db") + BanksiaDatabase(driver) + } +} diff --git a/core/sqld/src/jvmMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.jvm.kt b/core/sqld/src/jvmMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.jvm.kt new file mode 100644 index 0000000..61d9e95 --- /dev/null +++ b/core/sqld/src/jvmMain/kotlin/moe/lava/banksia/core/sqld/DatabaseManager.jvm.kt @@ -0,0 +1,56 @@ +package moe.lava.banksia.core.sqld + +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import moe.lava.banksia.core.util.error +import org.koin.core.component.KoinComponent +import java.io.File +import java.util.Properties +import kotlin.system.exitProcess + +actual class DatabaseManager : KoinComponent { + private var driver = connect() + actual val database get() = BanksiaDatabase(driver) + + private fun connect(path: String = "./data/${DBNAME}.db") = + JdbcSqliteDriver("jdbc:sqlite:${path}", Properties(), BanksiaDatabase.Schema) + .apply { execute(null, "PRAGMA journal_mode = OFF;", 0) } + + fun makeAlt() = run { + File("./data/${DBNAME}_alt.db").takeIf { it.exists() }?.delete() + val driver = connect("./data/${DBNAME}_alt.db") + BanksiaDatabase(driver) to { driver.close() } + } + + fun swap(scope: CoroutineScope = CoroutineScope(Dispatchers.IO)) { + val live = File("./data/${DBNAME}.db") + val alt = File("./data/${DBNAME}_alt.db") + val old = File("./data/${DBNAME}_old.db") + + if (live.takeIf { it.exists() }?.renameTo(old) == false) { + error("DatabaseManager", "Failed to rename database from live to old (${live.absolutePath} -> ${old.absolutePath})") + return + } + if (alt.takeIf { it.exists() }?.renameTo(live) == false) { + error("DatabaseManager", "Failed to rename database from alt to live, trying to undo.. (${alt.absolutePath} -> ${live.absolutePath})") + if (!live.renameTo(old)) { + error("DatabaseManager", "Failed to undo, critical failure, exiting..") + exitProcess(1) + } + return + } + val oldDriver = driver + driver = connect() + + scope.launch { + delay(5000) + if (old.takeIf { it.exists() }?.delete() == false) { + error("DatabaseManager", "Failed to unlink old database, stray files! (${old.absolutePath})") + } + oldDriver.close() + } + } +} diff --git a/shared/src/androidMain/kotlin/moe/lava/banksia/util/Logging.android.kt b/core/src/androidMain/kotlin/moe/lava/banksia/core/util/Logging.android.kt similarity index 87% rename from shared/src/androidMain/kotlin/moe/lava/banksia/util/Logging.android.kt rename to core/src/androidMain/kotlin/moe/lava/banksia/core/util/Logging.android.kt index 31c3072..e0b792e 100644 --- a/shared/src/androidMain/kotlin/moe/lava/banksia/util/Logging.android.kt +++ b/core/src/androidMain/kotlin/moe/lava/banksia/core/util/Logging.android.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.util +package moe.lava.banksia.core.util import android.util.Log diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton b/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt.skeleton similarity index 88% rename from shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton rename to core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt.skeleton index 15b3c58..909f642 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/Constants.kt.skeleton @@ -6,7 +6,7 @@ object Constants { const val opendataKey: String = "" const val serverUrl: String = "https://banksia.lava.moe/api/" // TODO - const val devMode: Boolean = false + var devMode: Boolean = false const val updateKey: String = "" const val protomapsKey: String = "" } diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/Endpoint.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/Endpoint.kt new file mode 100644 index 0000000..7e23b5d --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/Endpoint.kt @@ -0,0 +1,3 @@ +package moe.lava.banksia.core.endpoints + +object Endpoint diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/FutureTime.kt similarity index 94% rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/FutureTime.kt index 91c5c77..7c77309 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/FutureTime.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.model +package moe.lava.banksia.core.model import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate @@ -12,7 +12,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import moe.lava.banksia.model.FutureTime.Companion.asInt +import moe.lava.banksia.core.model.FutureTime.Companion.asInt @Serializable(FutureTimeSerialiser::class) data class FutureTime( diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Route.kt similarity index 82% rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/Route.kt index 9cfff0f..b2741f4 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Route.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.model +package moe.lava.banksia.core.model import kotlinx.serialization.Serializable diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/RouteType.kt similarity index 66% rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/RouteType.kt index 08a9c53..86555a6 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/RouteType.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.model +package moe.lava.banksia.core.model import kotlinx.serialization.Serializable @@ -13,4 +13,8 @@ enum class RouteType(val value: Int) { SkyBus(11), Interstate(10), ; + + companion object { + fun from(value: Int) = entries.first { it.value == value } + } } diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Run.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Run.kt similarity index 52% rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/Run.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/Run.kt index 328a4b0..69799bf 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Run.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Run.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.model +package moe.lava.banksia.core.model data class Run( val ref: String, diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Service.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Service.kt similarity index 87% rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/Service.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/Service.kt index a57fb82..8568397 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Service.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Service.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.model +package moe.lava.banksia.core.model import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/ServiceException.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/ServiceException.kt new file mode 100644 index 0000000..ef2f918 --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/ServiceException.kt @@ -0,0 +1,11 @@ +package moe.lava.banksia.core.model + +import kotlinx.datetime.LocalDate +import kotlinx.serialization.Serializable + +@Serializable +data class ServiceException( + val serviceId: String, + val date: LocalDate, + val type: Int, +) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Shape.kt similarity index 67% rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/Shape.kt index 6299ca0..7b71427 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Shape.kt @@ -1,7 +1,7 @@ -package moe.lava.banksia.model +package moe.lava.banksia.core.model import kotlinx.serialization.Serializable -import moe.lava.banksia.util.Point +import moe.lava.banksia.core.util.Point typealias ShapePath = List diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Stop.kt similarity index 53% rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/Stop.kt index df10a58..bbe6fbf 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Stop.kt @@ -1,15 +1,15 @@ -package moe.lava.banksia.model +package moe.lava.banksia.core.model import kotlinx.serialization.Serializable -import moe.lava.banksia.util.Point +import moe.lava.banksia.core.util.Point @Serializable data class Stop( val id: String, val name: String, val pos: Point, - val parent: String, + val parent: String?, val hasWheelChairBoarding: Boolean, - val level: String, - val platformCode: String, + val level: String?, + val platformCode: String?, ) diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StopTime.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StopTime.kt new file mode 100644 index 0000000..edd7c51 --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StopTime.kt @@ -0,0 +1,45 @@ +package moe.lava.banksia.core.model + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.Serializable + +@Serializable +data class StopTime( + val patternId: Long, + val stopId: String, + val time: T, + val pickupType: Int, + val dropOffType: Int, +) { + typealias Dated = StopTime + typealias Undated = StopTime +} + +@Serializable +sealed class TimeType { + @Serializable + data class Undated( + val arrival: FutureTime, + val departure: FutureTime, + ) : TimeType() + + @Serializable + data class Dated( + val arrival: LocalDateTime, + val departure: LocalDateTime, + ) : TimeType() +} + +fun TimeType.Undated.atDate(date: LocalDate) = TimeType.Dated( + arrival = arrival.atDate(date), + departure = departure.atDate(date), +) + +fun StopTime.atDate(date: LocalDate) = StopTime( + patternId = patternId, + stopId = stopId, + time = time.atDate(date), + pickupType = pickupType, + dropOffType = dropOffType, +) diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StoppingPattern.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StoppingPattern.kt new file mode 100644 index 0000000..1374cff --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/StoppingPattern.kt @@ -0,0 +1,16 @@ +package moe.lava.banksia.core.model + +import kotlinx.serialization.Serializable + +@Serializable +data class StoppingPattern( + val id: Long, + val routeId: String, + val shapeId: String, + val headsign: String, + val wheelchairAccessible: Boolean, + val stoptimes: List>, +) { + typealias Dated = StoppingPattern + typealias Undated = StoppingPattern +} diff --git a/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Trip.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Trip.kt new file mode 100644 index 0000000..752d6d2 --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/Trip.kt @@ -0,0 +1,15 @@ +package moe.lava.banksia.core.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Trip( + val id: String, + val pattern: StoppingPattern, + val service: Service, + val directionId: Int, + val blockId: String?, +) { + typealias Dated = Trip + typealias Undated = Trip +} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/VersionMetadata.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/VersionMetadata.kt similarity index 79% rename from shared/src/commonMain/kotlin/moe/lava/banksia/model/VersionMetadata.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/model/VersionMetadata.kt index 1770b23..2ee4f28 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/VersionMetadata.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/model/VersionMetadata.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.model +package moe.lava.banksia.core.model import kotlinx.serialization.Serializable diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/BoxedValue.kt similarity index 87% rename from shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/util/BoxedValue.kt index 3ff5702..f761518 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/BoxedValue.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.util +package moe.lava.banksia.core.util /** Wraps an arbitrary value, such that equality checks are forced to be done by reference */ class BoxedValue(val value: T) { diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/CacheMap.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/CacheMap.kt similarity index 97% rename from shared/src/commonMain/kotlin/moe/lava/banksia/util/CacheMap.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/util/CacheMap.kt index e41cef6..22236c6 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/CacheMap.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/CacheMap.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.util +package moe.lava.banksia.core.util import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/DayOfWeekExtension.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/DayOfWeekExtension.kt similarity index 96% rename from shared/src/commonMain/kotlin/moe/lava/banksia/util/DayOfWeekExtension.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/util/DayOfWeekExtension.kt index 87d3244..7feca0d 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/DayOfWeekExtension.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/DayOfWeekExtension.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.util +package moe.lava.banksia.core.util import kotlinx.datetime.DayOfWeek diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/Logging.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Logging.kt similarity index 88% rename from shared/src/commonMain/kotlin/moe/lava/banksia/util/Logging.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/util/Logging.kt index 7f26800..9d5f55a 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/Logging.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Logging.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.util +package moe.lava.banksia.core.util fun error(tag: String, throwable: Throwable) = error(tag, "", throwable) expect fun log(tag: String, msg: String) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/LoopFlow.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/LoopFlow.kt similarity index 98% rename from shared/src/commonMain/kotlin/moe/lava/banksia/util/LoopFlow.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/util/LoopFlow.kt index ee3e826..ec21d62 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/LoopFlow.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/LoopFlow.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.util +package moe.lava.banksia.core.util import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/Point.kt b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Point.kt similarity index 75% rename from shared/src/commonMain/kotlin/moe/lava/banksia/util/Point.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/core/util/Point.kt index 4aae7d4..4db05e2 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/Point.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/core/util/Point.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.util +package moe.lava.banksia.core.util import kotlinx.serialization.Serializable diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt similarity index 97% rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt index 77ab12d..54717a2 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/PtvService.kt @@ -16,7 +16,12 @@ import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.delay import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import moe.lava.banksia.Constants +import moe.lava.banksia.core.Constants +import moe.lava.banksia.core.model.RouteType +import moe.lava.banksia.core.util.LoopFlow.Companion.initWith +import moe.lava.banksia.core.util.error +import moe.lava.banksia.core.util.log +import moe.lava.banksia.core.util.loopFlow import moe.lava.banksia.data.ptv.structures.PtvDeparture import moe.lava.banksia.data.ptv.structures.PtvDirection import moe.lava.banksia.data.ptv.structures.PtvRoute @@ -24,11 +29,6 @@ import moe.lava.banksia.data.ptv.structures.PtvRouteType import moe.lava.banksia.data.ptv.structures.PtvRouteType.Companion.asPtvType import moe.lava.banksia.data.ptv.structures.PtvRun import moe.lava.banksia.data.ptv.structures.PtvStop -import moe.lava.banksia.model.RouteType -import moe.lava.banksia.util.LoopFlow.Companion.initWith -import moe.lava.banksia.util.error -import moe.lava.banksia.util.log -import moe.lava.banksia.util.loopFlow import okio.ByteString.Companion.encodeUtf8 import kotlin.random.Random diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt similarity index 100% rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDeparture.kt diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt similarity index 100% rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvDirection.kt diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt similarity index 100% rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvGeopath.kt diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt similarity index 94% rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt index 3178328..4aae762 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt @@ -2,7 +2,7 @@ package moe.lava.banksia.data.ptv.structures import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import moe.lava.banksia.model.RouteType +import moe.lava.banksia.core.model.RouteType @Serializable data class PtvRoute( diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt similarity index 97% rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt index c9988bf..d8808f1 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt +++ b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import moe.lava.banksia.model.RouteType +import moe.lava.banksia.core.model.RouteType object PtvRouteTypeSerialiser : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt similarity index 100% rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRun.kt diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt b/core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt similarity index 100% rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt rename to core/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvStop.kt diff --git a/shared/src/iosMain/kotlin/moe/lava/banksia/util/Logging.ios.kt b/core/src/iosMain/kotlin/moe/lava/banksia/core/util/Logging.ios.kt similarity index 83% rename from shared/src/iosMain/kotlin/moe/lava/banksia/util/Logging.ios.kt rename to core/src/iosMain/kotlin/moe/lava/banksia/core/util/Logging.ios.kt index b58b89a..014c1d2 100644 --- a/shared/src/iosMain/kotlin/moe/lava/banksia/util/Logging.ios.kt +++ b/core/src/iosMain/kotlin/moe/lava/banksia/core/util/Logging.ios.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.util +package moe.lava.banksia.core.util actual fun log(tag: String, msg: String) { TODO("Not yet implemented") diff --git a/shared/src/jvmMain/kotlin/moe/lava/banksia/util/Logging.jvm.kt b/core/src/jvmMain/kotlin/moe/lava/banksia/core/util/Logging.jvm.kt similarity index 86% rename from shared/src/jvmMain/kotlin/moe/lava/banksia/util/Logging.jvm.kt rename to core/src/jvmMain/kotlin/moe/lava/banksia/core/util/Logging.jvm.kt index 0a1ea10..de7fdaa 100644 --- a/shared/src/jvmMain/kotlin/moe/lava/banksia/util/Logging.jvm.kt +++ b/core/src/jvmMain/kotlin/moe/lava/banksia/core/util/Logging.jvm.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.util +package moe.lava.banksia.core.util actual fun log(tag: String, msg: String) { println("[$tag] $msg") diff --git a/core/stoptime/build.gradle.kts b/core/stoptime/build.gradle.kts new file mode 100644 index 0000000..44cf072 --- /dev/null +++ b/core/stoptime/build.gradle.kts @@ -0,0 +1,64 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.ksp) +} + +kotlin { + android { + namespace = "moe.lava.banksia.core.stoptime" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + freeCompilerArgs.add("-Xexpect-actual-classes") + } + + iosArm64() + iosSimulatorArm64() + + jvm() + + sourceSets { + val clientMain by creating { + dependsOn(commonMain.get()) + } + + androidMain.get().dependsOn(clientMain) + iosArm64Main.get().dependsOn(clientMain) + iosSimulatorArm64Main.get().dependsOn(clientMain) + + androidMain.dependencies { + implementation(libs.ktor.client.okhttp) + } + commonMain.dependencies { + implementation(libs.okio) + implementation(libs.koin.core) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.contentnegotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.serialization.protobuf) + + implementation(projects.core) + implementation(projects.core.sqld) + } + iosMain.dependencies { + implementation(libs.ktor.client.darwin) + } + jvmMain.dependencies { + implementation(libs.koin.ktor) + implementation(libs.ktor.server.core) + } + } +} diff --git a/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.client.kt b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.client.kt new file mode 100644 index 0000000..2f83304 --- /dev/null +++ b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.client.kt @@ -0,0 +1,11 @@ +package moe.lava.banksia.core.data + +import moe.lava.banksia.core.data.repositories.StopTimeRepository +import moe.lava.banksia.core.data.sources.stoptime.StopTimeRemoteDataSource +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +internal actual val platformModule = module { + singleOf(::StopTimeRepository) + singleOf(::StopTimeRemoteDataSource) +} diff --git a/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.client.kt b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.client.kt new file mode 100644 index 0000000..ecaff8e --- /dev/null +++ b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.client.kt @@ -0,0 +1,19 @@ +package moe.lava.banksia.core.data.repositories + +import kotlinx.coroutines.flow.flow +import kotlinx.datetime.LocalDate +import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource +import moe.lava.banksia.core.data.sources.stoptime.StopTimeRemoteDataSource + +actual class StopTimeRepository internal constructor( + private val local: StopTimeLocalDataSource, + private val remote: StopTimeRemoteDataSource, +) { + actual suspend fun getForStop(id: String, date: LocalDate) = flow { + emit(local.getAtStop(id, date)) + + remote.getAtStop(id, date) + .takeIf { it.isNotEmpty() } + ?.let { emit(it) } + } +} diff --git a/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeRemoteDataSource.kt b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeRemoteDataSource.kt new file mode 100644 index 0000000..0c38f64 --- /dev/null +++ b/core/stoptime/src/clientMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeRemoteDataSource.kt @@ -0,0 +1,26 @@ +package moe.lava.banksia.core.data.sources.stoptime + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.todayIn +import moe.lava.banksia.core.data.dto.ExtendedStopTime +import moe.lava.banksia.core.endpoints.Endpoint +import moe.lava.banksia.core.endpoints.stopTimeByStop +import kotlin.time.Clock + +internal class StopTimeRemoteDataSource( + private val client: HttpClient, +) { + suspend fun getAtStop( + stopId: String, + date: LocalDate? = Clock.System.todayIn(TimeZone.currentSystemDefault()), + ): List { + return client.get(Endpoint.stopTimeByStop(stopId)) { + parameter("date", date) + }.body>() + } +} diff --git a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.kt b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.kt new file mode 100644 index 0000000..d46affa --- /dev/null +++ b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.kt @@ -0,0 +1,13 @@ +package moe.lava.banksia.core.data + +import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource +import org.koin.core.module.Module +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +internal expect val platformModule: Module; + +val stopTimeDataDiModule = module { + includes(platformModule) + singleOf(::StopTimeLocalDataSource) +} diff --git a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/dto/ExtendedStopTime.kt b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/dto/ExtendedStopTime.kt new file mode 100644 index 0000000..38de29d --- /dev/null +++ b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/dto/ExtendedStopTime.kt @@ -0,0 +1,34 @@ +package moe.lava.banksia.core.data.dto + +import kotlinx.datetime.LocalDate +import kotlinx.serialization.Serializable +import moe.lava.banksia.core.model.FutureTime +import moe.lava.banksia.core.model.RouteType +import moe.lava.banksia.core.model.TimeType +import moe.lava.banksia.core.model.atDate +import moe.lava.banksia.core.sqld.GetExtendedForStop + +@Serializable +data class ExtendedStopTime( + val patternId: Long, + val stopPlatformCode: String?, + val time: TimeType.Dated, + val headsign: String?, + val routeType: RouteType, + val routeNumber: String?, + val routeName: String, +) + +// TODO: This probably doesn't belong here +fun GetExtendedForStop.asModel(date: LocalDate) = ExtendedStopTime( + patternId = patternId, + stopPlatformCode = stopPlatformCode, + time = TimeType.Undated( + arrival = FutureTime.fromInt((departureTime + arrivalDelta).toInt()), + departure = FutureTime.fromInt(departureTime.toInt()), + ).atDate(date), + headsign = headsign, + routeType = RouteType.from(routeType.toInt()), + routeNumber = routeNumber, + routeName = routeName, +) diff --git a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.kt b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.kt new file mode 100644 index 0000000..6a81c09 --- /dev/null +++ b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.kt @@ -0,0 +1,15 @@ +package moe.lava.banksia.core.data.repositories + +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.todayIn +import moe.lava.banksia.core.data.dto.ExtendedStopTime +import kotlin.time.Clock + +expect class StopTimeRepository { + suspend fun getForStop( + id: String, + date: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), + ): Flow> +} diff --git a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeLocalDataSource.kt b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeLocalDataSource.kt new file mode 100644 index 0000000..f22dc09 --- /dev/null +++ b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeLocalDataSource.kt @@ -0,0 +1,30 @@ +package moe.lava.banksia.core.data.sources.stoptime + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.withContext +import kotlinx.datetime.LocalDate +import moe.lava.banksia.core.data.dto.ExtendedStopTime +import moe.lava.banksia.core.data.dto.asModel +import moe.lava.banksia.core.sqld.StopTimeQueries +import moe.lava.banksia.core.util.serialise +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +internal class StopTimeLocalDataSource : KoinComponent { + private val queries get() = get() + + suspend fun getAtStop(stopId: String, date: LocalDate): List { + return withContext(context = Dispatchers.IO) { + queries + .getExtendedForStop( + listOf(date.dayOfWeek).serialise().toLong(), + date.toEpochDays(), + stopId, + ) + .executeAsList() + .map { it.asModel(date) } + .sortedBy { it.time.departure } + } + } +} diff --git a/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/StopTimeEndpoints.kt b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/StopTimeEndpoints.kt new file mode 100644 index 0000000..f689b2d --- /dev/null +++ b/core/stoptime/src/commonMain/kotlin/moe/lava/banksia/core/endpoints/StopTimeEndpoints.kt @@ -0,0 +1,3 @@ +package moe.lava.banksia.core.endpoints + +fun Endpoint.stopTimeByStop(stopId: String) = "stoptimes/by_stop/${stopId}" diff --git a/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.jvm.kt b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.jvm.kt new file mode 100644 index 0000000..70ef406 --- /dev/null +++ b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/StopTimeDataDiModule.jvm.kt @@ -0,0 +1,9 @@ +package moe.lava.banksia.core.data + +import moe.lava.banksia.core.data.repositories.StopTimeRepository +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +internal actual val platformModule = module { + singleOf(::StopTimeRepository) +} diff --git a/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.jvm.kt b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.jvm.kt new file mode 100644 index 0000000..b4c37a6 --- /dev/null +++ b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.jvm.kt @@ -0,0 +1,13 @@ +package moe.lava.banksia.core.data.repositories + +import kotlinx.coroutines.flow.flow +import kotlinx.datetime.LocalDate +import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource + +actual class StopTimeRepository internal constructor( + private val local: StopTimeLocalDataSource, +) { + actual suspend fun getForStop(id: String, date: LocalDate) = flow { + emit(local.getAtStop(id, date)) + } +} diff --git a/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/server/routes/StopTimeRoute.kt b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/server/routes/StopTimeRoute.kt new file mode 100644 index 0000000..5791855 --- /dev/null +++ b/core/stoptime/src/jvmMain/kotlin/moe/lava/banksia/server/routes/StopTimeRoute.kt @@ -0,0 +1,27 @@ +package moe.lava.banksia.server.routes + +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import kotlinx.coroutines.flow.first +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.todayIn +import moe.lava.banksia.core.data.repositories.StopTimeRepository +import moe.lava.banksia.core.endpoints.Endpoint +import moe.lava.banksia.core.endpoints.stopTimeByStop +import org.koin.ktor.ext.inject +import kotlin.time.Clock + +fun Route.stopTimeRoutes() { + val repo by inject() + + get(Endpoint.stopTimeByStop("{stop_id}")) { + val stopId = call.parameters["stop_id"]!! + val date = call.queryParameters["date"] + ?.let { LocalDate.parse(it, LocalDate.Formats.ISO) } + ?: Clock.System.todayIn(TimeZone.currentSystemDefault()) + val data = repo.getForStop(stopId, date).first() + call.respond(data) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 70676f5..483c5d5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,40 +1,36 @@ [versions] agp = "9.1.0" -android-compileSdk = "36" +android-compileSdk = "37" android-minSdk = "24" -android-targetSdk = "36" -androidx-activityCompose = "1.12.4" -androidx-appcompat = "1.7.0" -androidx-constraintlayout = "2.2.1" -androidx-core-ktx = "1.15.0" -androidx-espresso-core = "3.6.1" -androidx-lifecycle = "2.9.6" -androidx-material = "1.12.0" -androidx-test-junit = "1.2.1" -compose-multiplatform = "1.11.0-alpha02" +android-targetSdk = "37" +androidx-activity= "1.13.0" +androidx-lifecycle = "2.10.0" +compose-multiplatform = "1.12.0-alpha02" composeunstyled = "1.49.6" coroutines = "1.10.2" geo = "0.8.0" -junit = "4.13.2" -koin = "4.1.1" -kotlin = "2.3.10" +koin = "4.2.0" +kotlin = "2.3.20" kotlinxDatetime = "0.7.1" kotlinxSerializationCsv = "0.2.18" kotlinxSerialization = "1.10.0" ksp = "2.3.4" -ktor = "3.4.0" +ktor = "3.4.1" logback = "1.5.32" maplibre = "0.12.1" material = "1.7.3" -material3 = "1.11.0-alpha02" -okio = "3.16.4" +material3 = "1.11.0-alpha07" +okio = "3.17.0" playServicesLocation = "21.3.0" -sqlite = "2.6.2" -room = "2.8.4" secretsGradlePlugin = "2.0.1" -wire = "5.5.0" +sqldelight = "2.3.2" +wire = "6.1.0" [libraries] +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } +androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } +androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-multiplatform" } compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-multiplatform" } compose-material-icons-core = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "material" } @@ -44,18 +40,11 @@ compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-mu compose-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "compose-multiplatform" } compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "compose-multiplatform" } composeunstyled = { module = "com.composables:composeunstyled", version.ref = "composeunstyled" } -moko-geo = { module = "dev.icerock.moko:geo", version.ref = "geo" } -moko-geo-compose = { module = "dev.icerock.moko:geo-compose", version.ref = "geo" } -kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } -androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } -androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } -androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } -androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } -koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" } +kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } @@ -73,14 +62,14 @@ ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "k ktor-server-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre" } +moko-geo = { module = "dev.icerock.moko:geo", version.ref = "geo" } +moko-geo-compose = { module = "dev.icerock.moko:geo-compose", version.ref = "geo" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" } -room-common = { group = "androidx.room", name = "room-common", version.ref = "room" } -room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } -room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } -room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } -sqlite-bundled = { group = "androidx.sqlite", name = "sqlite-bundled", version.ref = "sqlite" } secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } +sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } +sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } +sqldelight-driver-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" } ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "compose-multiplatform" } [plugins] @@ -93,6 +82,6 @@ kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } ktor = { id = "io.ktor.plugin", version.ref = "ktor" } -room = { id = "androidx.room", version.ref = "room" } secretsGradle = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" } +sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } wire = { id = "com.squareup.wire", version.ref = "wire" } diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 2f7d989..9d2cb78 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -5,15 +5,27 @@ plugins { application } -group = "moe.lava.banksia" +group = "moe.lava.banksia.server" version = "1.0.0" application { mainClass.set("moe.lava.banksia.server.ApplicationKt") applicationDefaultJvmArgs = listOf("-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}") } +kotlin { + compilerOptions { + freeCompilerArgs.add("-Xexplicit-backing-fields") + } +} + dependencies { - implementation(projects.shared) + implementation(projects.core) + implementation(projects.core.data) + implementation(projects.core.sqld) + implementation(projects.core.stoptime) + implementation(projects.server.gtfs) + implementation(projects.server.gtfsRt) + implementation(libs.logback) implementation(libs.koin.core) implementation(libs.koin.ktor) @@ -26,8 +38,6 @@ dependencies { implementation(libs.ktor.server.contentnegotiation) implementation(libs.ktor.server.core) implementation(libs.ktor.server.netty) - implementation(libs.room.runtime) - implementation(libs.sqlite.bundled) testImplementation(libs.ktor.server.tests) testImplementation(libs.kotlin.test.junit) } diff --git a/server/gtfs/build.gradle.kts b/server/gtfs/build.gradle.kts new file mode 100644 index 0000000..8f6d646 --- /dev/null +++ b/server/gtfs/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.kotlinSerialization) +} + +kotlin { + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + freeCompilerArgs.add("-Xexplicit-backing-fields") + } +} + +dependencies { + implementation(projects.core) + implementation(libs.kotlinx.serialization.csv) + implementation(libs.kotlinx.datetime) + implementation(libs.ktor.client.contentnegotiation) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.okhttp) +} diff --git a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt new file mode 100644 index 0000000..c844499 --- /dev/null +++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt @@ -0,0 +1,388 @@ +package moe.lava.banksia.server.gtfs + +import com.lightningkite.kotlinx.serialization.csv.CsvFormat +import com.lightningkite.kotlinx.serialization.csv.StringDeferringConfig +import io.ktor.client.HttpClient +import io.ktor.client.request.prepareRequest +import io.ktor.client.request.url +import io.ktor.client.statement.bodyAsChannel +import io.ktor.util.cio.writeChannel +import io.ktor.util.logging.Logger +import io.ktor.utils.io.copyAndClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.serializer +import moe.lava.banksia.core.Constants +import moe.lava.banksia.core.model.FutureTime.Companion.asInt +import moe.lava.banksia.core.model.Route +import moe.lava.banksia.core.model.RouteType +import moe.lava.banksia.core.model.Service +import moe.lava.banksia.core.model.ServiceException +import moe.lava.banksia.core.model.Shape +import moe.lava.banksia.core.model.Stop +import moe.lava.banksia.core.model.StopTime +import moe.lava.banksia.core.model.StoppingPattern +import moe.lava.banksia.core.model.TimeType +import moe.lava.banksia.core.model.Trip +import moe.lava.banksia.core.util.Point +import moe.lava.banksia.server.gtfs.structures.GtfsRoute +import moe.lava.banksia.server.gtfs.structures.GtfsService +import moe.lava.banksia.server.gtfs.structures.GtfsServiceException +import moe.lava.banksia.server.gtfs.structures.GtfsShape +import moe.lava.banksia.server.gtfs.structures.GtfsStop +import moe.lava.banksia.server.gtfs.structures.GtfsStopTime +import moe.lava.banksia.server.gtfs.structures.GtfsTrip +import java.io.File +import java.nio.ByteBuffer +import java.security.MessageDigest +import java.util.zip.ZipFile +import kotlin.time.ExperimentalTime + +private typealias StopWithSource = Pair + +sealed class GtfsData { + data class RouteChunk(val routes: List) : GtfsData() + data class ServiceChunk(val services: List) : GtfsData() + data class ServiceExceptionChunk(val exceptions: List) : GtfsData() + data class ShapeChunk(val shapes: List) : GtfsData() + data class StopChunk(val stops: List) : GtfsData() + data class TripChunk(val trips: List) : GtfsData() +} + +class GtfsParser( + private val log: Logger, + private val client: HttpClient, +) { + private val csv = CsvFormat(StringDeferringConfig(EmptySerializersModule())) + private val datasetPath = File("/tmp/banksia", "dataset.zip") + + @OptIn(ExperimentalTime::class) + suspend fun update(datasetUrl: String): Flow { + val parentDir = datasetPath.parentFile + @Suppress("SimplifyBooleanWithConstants", "KotlinConstantConditions") + if (parentDir.exists() && !Constants.devMode) + parentDir.deleteRecursively() + + parentDir.mkdirs() + + log.info("fetching..") + client.prepareRequest { + url(datasetUrl) + }.execute { resp -> + if (!datasetPath.exists()) + resp.bodyAsChannel().copyAndClose(datasetPath.writeChannel()) + log.info("fetched!") + } + + log.info("extracting...") + @Suppress("KotlinConstantConditions") + val files = if (Constants.devMode) { + datasetPath.parentFile + .listFiles { it.isDirectory } + .flatMap { d -> d.listFiles { f -> f.extension == "txt" }.toList() } + .ifEmpty { extractAll(datasetPath) } +// .filter { it.parentFile.name == "2" } + } else { + extractAll(datasetPath) + } + + log.info("parsing...") + return parse(files) + .onCompletion { + @Suppress("KotlinConstantConditions") + if (!Constants.devMode) { + parentDir.deleteRecursively() + } + + log.info("done!") + } + } + + private fun parse(files: List) = flow { + files + .filter { it.name == "routes.txt" } + .forEach { emit(GtfsData.RouteChunk(parseRoutes(it))) } + + files + .filter { it.name == "stops.txt" } + .flatMap { parseStops(it) } + .let { emit(GtfsData.StopChunk(fixupDuplicateStops(it))) } + + files + .filter { it.name == "shapes.txt" } + .forEach { emit(GtfsData.ShapeChunk(parseShapes(it))) } + + val services = files + .filter { it.name == "calendar.txt" } + .flatMap { fd -> + parseServices(fd) + .also { emit(GtfsData.ServiceChunk(it)) } + } + .associateBy { it.id } + + files + .filter { it.name == "calendar_dates.txt" } + .forEach { emit(GtfsData.ServiceExceptionChunk(parseServiceExceptions(it))) } + + val trips = files + .filter { it.name == "trips.txt" } + .flatMap { fd -> + parseTrips(fd, services) + } + .associateBy { it.id } + + files + .filter { it.name == "stop_times.txt" } + .forEach { fd -> + log.info("parsing stop times for ${fd.parent}...") + parseStopTimes(fd) { seq -> + val times = ArrayList>(1000100) + seq.forEach { pair -> + val (_, stoptime) = pair + if (times.size > 1000000 && stoptime.patternId == 1L) { + emit(GtfsData.TripChunk(processStoptimes(trips, times))) + times.clear() + } + + times.add(pair) + } + emit(GtfsData.TripChunk(processStoptimes(trips, times))) + } + } + } + + private fun hashCalc(headsign: String, stops: List): Long { + val inst = MessageDigest.getInstance("SHA-256") + inst.update(headsign.toByteArray()) + stops.forEach { + inst.update(it.stopId.toByteArray()) + val dint = it.time.departure.asInt() + inst.update((dint).toByte()) + inst.update((dint shr 8).toByte()) + val aint = it.time.arrival.asInt() + inst.update((aint).toByte()) + inst.update((aint shr 8).toByte()) + } + + val buf = inst.digest().slice(0..7).toByteArray() + buf[0] = 0 + buf[1] = 0 + return ByteBuffer.wrap(buf).long + } + + private fun processStoptimes(trips: Map, times: ArrayList>) = + times.groupBy { it.first } + .map { (tripId, pairs) -> + val trip = trips[tripId]!! + val stoptimes = pairs.map { it.second } + val hash = hashCalc(trip.pattern.headsign, stoptimes) + trip.copy(pattern = trip.pattern.copy( + id = hash, + stoptimes = stoptimes.map { it.copy(patternId = hash) } + )) + } + + private fun parseRoutes(fd: File) = + fd.parseCsv() + .map { with(it) { + Route( + id = route_id, + type = RouteType.from(fd.parentFile.name.toInt()), + number = route_short_name, + name = route_long_name, + ) + } } + + private fun parseShapes(fd: File) = + fd.parseCsv() + .groupBy { it.shape_id } + .map { (id, group) -> + val points = group + .sortedBy { it.shape_pt_sequence } + .map { Point(it.shape_pt_lat, it.shape_pt_lon) } + + Shape(id, points) + } + + private fun parseStops(fd: File): List = + fd.parseCsv() + .map { with(it) { + fd.parentFile.name to Stop( + id = stop_id, + name = stop_name, + pos = Point(stop_lat, stop_lon), + parent = parent_station.ifEmpty { null }, + hasWheelChairBoarding = wheelchair_boarding == "1", + level = level_id.ifEmpty { null }, + platformCode = platform_code.ifEmpty { null }, + ) + } } + + private inline fun parseStopTimes(fd: File, block: (Sequence>) -> Unit) = + fd.parseCsvSequence { seq -> + seq + .map { with(it) { + it.trip_id to StopTime( + patternId = stop_sequence, + stopId = stop_id, + time = TimeType.Undated( + arrival = GtfsStopTime.parseGtfsTime(arrival_time), + departure = GtfsStopTime.parseGtfsTime(departure_time), + ), + pickupType = pickup_type, + dropOffType = drop_off_type, + ) + } } + .let { block(it) } + } + + private fun parseServices(fd: File) = + fd.parseCsv() + .map { with(it) { + val days = buildList { + if (monday == 1) add(DayOfWeek.MONDAY) + if (tuesday == 1) add(DayOfWeek.TUESDAY) + if (wednesday == 1) add(DayOfWeek.WEDNESDAY) + if (thursday == 1) add(DayOfWeek.THURSDAY) + if (friday == 1) add(DayOfWeek.FRIDAY) + if (saturday == 1) add(DayOfWeek.SATURDAY) + if (sunday == 1) add(DayOfWeek.SUNDAY) + } + Service( + id = "${fd.parentFile.name}_${service_id}", + days = days, + start = LocalDate.parse(start_date, LocalDate.Formats.ISO_BASIC), + end = LocalDate.parse(end_date, LocalDate.Formats.ISO_BASIC), + ) + } } + + private fun parseServiceExceptions(fd: File) = + fd.parseCsv() + .map { with(it) { + ServiceException( + serviceId = "${fd.parentFile.name}_${service_id}", + date = LocalDate.parse(date, LocalDate.Formats.ISO_BASIC), + type = exception_type, + ) + } } + + private fun parseTrips(fd: File, services: Map) = + fd.parseCsv() + .map { with(it) { + Trip.Undated( + id = trip_id, + pattern = StoppingPattern( + id = 0, + routeId = route_id, + shapeId = shape_id, + headsign = trip_headsign, + wheelchairAccessible = wheelchair_accessible == "1", + stoptimes = listOf() + ), + service = services["${fd.parentFile.name}_${service_id}"]!!, + directionId = direction_id.toInt(), + blockId = block_id.ifEmpty { null }, + ) + } } + + private fun extract(fd: File): List { + val outputs = mutableListOf() + ZipFile(fd).use { zip -> + for (entry in zip.entries()) { + zip.getInputStream(entry).use { input -> + val out = File(fd.parentFile, entry.name) + out.parentFile.mkdirs() + out.outputStream().use { output -> + input.copyTo(output) + } + outputs.add(out) + } + } + } + return outputs + } + + private fun extractAll(fd: File) = extract(fd).flatMap(::extract) + + private inline fun File.parseCsv(): List = this + .readText() + .replace("\uFEFF", "") // remove bom + .replace("\r\n", "\n") // crlf -> lf + .let { csv.decodeFromString(it) } + + private inline fun File.parseCsvSequence(block: (Sequence) -> Unit) = this + .bufferedReader() + .use { reader -> + val iter = object : CharIterator() { + var next: Char? = null + override fun nextChar(): Char { + if (!hasNext()) { + throw NoSuchElementException() + } + val ret = next!! + next = null + return ret + } + override fun hasNext(): Boolean { + if (next == null) { + do { + next = null + val new = reader.read() + if (new != -1) { + next = new.toChar() + } + } while (next == '\uFEFF' || next == '\r') + } + return next != null + } + } + block(csv.decodeToSequence(iter, csv.serializersModule.serializer())) + } + + // Type priority used to resolve duplicates, preferring the first one in the chain + private val typePriorityRanking = listOf( + RouteType.MetroTrain, + RouteType.RegionalTrain, + RouteType.MetroTram, + RouteType.MetroBus, + RouteType.RegionalBus, + RouteType.SkyBus, + ).map { it.value.toString() } + + @Suppress("LoggingStringTemplateAsArgument") // ? + private fun fixupDuplicateStops(stops: List): List { + return stops + .groupBy { (_, stops) -> stops.id } + .map { (id, stops) -> + // Just return it if no duplicate + if (stops.size == 1) return@map stops[0].second + + // Just return the first one if all the stops' data match + if (stops.withIndex().all { (idx, stop) -> idx == 0 || stop.second == stops[idx - 1].second }) + return@map stops[0].second + + // Find first stop ordered by the types + val res = typePriorityRanking + .firstNotNullOfOrNull { type -> + stops.find { it.first == type } + } + + val (_, stop) = if (res == null) { + log.warn("Cannot resolve duplicate stop ${id}, using first one") + stops.forEach { (type, stop) -> log.warn(" - ($type): $stop") } + stops[0] + } else { + log.debug("Resolving $id for type ${res.first}") + stops.forEach { (type, stop) -> log.debug("${if (res.first == type) "*" else " "} - ($type): $stop") } + res + } + + stop + } + } +} diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt similarity index 91% rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt index c4eabeb..4b1bad9 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt +++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable @Suppress("PropertyName") @Serializable -data class GtfsRoute( +internal data class GtfsRoute( val route_id: String, val agency_id: String, val route_short_name: String, diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt similarity index 91% rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt index 9347b5e..1bf9573 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt +++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable @Suppress("PropertyName") @Serializable -data class GtfsService( +internal data class GtfsService( val service_id: String, val monday: Int, val tuesday: Int, diff --git a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsServiceException.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsServiceException.kt new file mode 100644 index 0000000..a31aff0 --- /dev/null +++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsServiceException.kt @@ -0,0 +1,11 @@ +package moe.lava.banksia.server.gtfs.structures + +import kotlinx.serialization.Serializable + +@Suppress("PropertyName") +@Serializable +internal data class GtfsServiceException( + val service_id: String, + val date: String, + val exception_type: Int, +) diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt similarity index 90% rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt index 19cdfb5..32231ab 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt +++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable @Suppress("PropertyName") @Serializable -data class GtfsShape( +internal data class GtfsShape( val shape_id: String, val shape_pt_lat: Double, val shape_pt_lon: Double, diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt similarity index 92% rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt index 023a3e1..cb1a018 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt +++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStop.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable @Suppress("PropertyName") @Serializable -data class GtfsStop( +internal data class GtfsStop( val stop_id: String, val stop_name: String, val stop_lat: Double, diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt similarity index 84% rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt index 61e8a1c..c0bbaf2 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt +++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsStopTime.kt @@ -1,16 +1,16 @@ package moe.lava.banksia.server.gtfs.structures import kotlinx.serialization.Serializable -import moe.lava.banksia.model.FutureTime +import moe.lava.banksia.core.model.FutureTime @Suppress("PropertyName") @Serializable -data class GtfsStopTime( +internal data class GtfsStopTime( val trip_id: String, val arrival_time: String, val departure_time: String, val stop_id: String, - val stop_sequence: Int, + val stop_sequence: Long, val stop_headsign: String, val pickup_type: Int, val drop_off_type: Int, diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt similarity index 92% rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt index fcfc864..0b0d865 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt +++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsTrip.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable @Suppress("PropertyName") @Serializable -data class GtfsTrip( +internal data class GtfsTrip( val route_id: String, val service_id: String, val trip_id: String, diff --git a/server/gtfs_rt/build.gradle.kts b/server/gtfs_rt/build.gradle.kts new file mode 100644 index 0000000..2887e0b --- /dev/null +++ b/server/gtfs_rt/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.wire) +} + +kotlin { + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + freeCompilerArgs.add("-Xexplicit-backing-fields") + } +} + +dependencies { + implementation(projects.core) + implementation(libs.okio) + implementation(libs.koin.core) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.contentnegotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.serialization.protobuf) +} + +wire { + sourcePath { + srcDir("src/main/proto") + } + kotlin {} +} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/gtfsr/GtfsRealtime.kt b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsRealtime.kt similarity index 89% rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/gtfsr/GtfsRealtime.kt rename to server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsRealtime.kt index 172238f..128f141 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/data/gtfsr/GtfsRealtime.kt +++ b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsRealtime.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.data.gtfsr +package moe.lava.banksia.server.gtfsrt import com.google.transit.realtime.FeedMessage diff --git a/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtArchiver.kt b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtArchiver.kt new file mode 100644 index 0000000..aaee0a9 --- /dev/null +++ b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtArchiver.kt @@ -0,0 +1,116 @@ +package moe.lava.banksia.server.gtfsrt + +import com.google.transit.realtime.FeedMessage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import moe.lava.banksia.core.util.log +import java.io.File +import kotlin.time.Instant + +private const val BASE_DIR = "./data/gtfsr-archive/" + +internal class GtfsrtArchiver { + private var started = false + + suspend fun start(flow: SharedFlow>) { + if (started) { + log("GtfsrtArchiver", "Tried to start when already started") + return + } + started = true + coroutineScope { + launch { compressJob() } + + flow.collect { (type, rawData) -> + val data = try { + FeedMessage.ADAPTER.decode(rawData) + } catch (e: Throwable) { + log("gtfsr $type", "Failed to parse proto: $e") + return@collect + } + val timestamp = data.header_.timestamp + ?: return@collect log("gtfsr $type", "Failed to read proto timestamp") + + val time = Instant.fromEpochSeconds(timestamp).toLocalDateTime(TimeZone.currentSystemDefault()) + + val (prevWeek, prevDay) = (time.dayOfYear - 1) / 7 to (time.dayOfYear - 1) % 7 + val (nextWeek, nextDay) = time.dayOfYear / 7 to time.dayOfYear % 7 + + val base = File(BASE_DIR, type) + val previousParent = File(base, "${time.year}-${prevWeek.toString().padStart(2, '0')}/${prevDay}") + val currentParent = File(base, "${time.year}-${nextWeek.toString().padStart(2, '0')}/${nextDay}") + val target = File(currentParent, "${timestamp}.proto") + + if (previousParent.isDirectory) { + enqueueCompression(previousParent) + if (prevWeek != nextWeek) { + enqueueCompression(previousParent.parentFile) + } + } + + if (!target.exists()) { + try { + if (!target.parentFile.isDirectory) { + target.parentFile.mkdirs() + } + target.writeBytes(rawData) + } catch (e: Throwable) { + log("gtfsr $type", "Failed to write ${target}: $e") + } + } + } + } + } + + private val cqueue = mutableSetOf() + private val ignore = mutableSetOf() + private val cmut = Mutex() + private suspend fun enqueueCompression(fd: File) { + cmut.withLock { cqueue.add(fd) } + } + + private suspend fun compressJob() { + while(true) { + while(true) { + val next = cmut.withLock { cqueue.firstOrNull() } + ?: break + if (!next.isDirectory) { + cmut.withLock { cqueue.remove(next) } + continue + } + if (next in ignore) continue + + withContext(Dispatchers.IO) { + val proc = ProcessBuilder( + "tar", "-acf", + "${next.absolutePath}.tar.zst", + next.absolutePath + ).start() + val exitCode = proc.waitFor() + if (exitCode == 0) { + log("CompressJob", "Compressed ${next.absolutePath} to ${next.absolutePath}.tar.zst") + if (next.deleteRecursively()) { + cmut.withLock { cqueue.remove(next) } + } else { + log("CompressJob", "Failed to delete $next") + ignore.add(next) + } + } else { + val msg = proc.errorStream.readAllBytes().decodeToString() + log("CompressJob", "Failed to delete $next (exit code $exitCode") + log("CompressJob", msg) + } + } + } + delay(30000) + } + } +} diff --git a/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtService.kt b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtService.kt new file mode 100644 index 0000000..6f46ed7 --- /dev/null +++ b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtService.kt @@ -0,0 +1,87 @@ +package moe.lava.banksia.server.gtfsrt + +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.url +import io.ktor.client.statement.bodyAsText +import io.ktor.client.statement.readRawBytes +import io.ktor.http.isSuccess +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import moe.lava.banksia.core.Constants +import moe.lava.banksia.core.util.LogScope +import moe.lava.banksia.core.util.log + +private val types = arrayOf( + "metro/trip-updates", + "metro/vehicle-positions", + "metro/service-alerts", + "tram/trip-updates", + "tram/vehicle-positions", + "tram/service-alerts", + "bus/trip-updates", + "bus/vehicle-positions", + "vline/trip-updates", + "vline/vehicle-positions", +) + +class GtfsrtService( + private val client: HttpClient, +) { + private val archiver = GtfsrtArchiver() + private var started = false + + internal val rawMessages: SharedFlow> + field = MutableSharedFlow>() + + fun start( + scope: CoroutineScope, + enableArchiving: Boolean = false, + ) { + if (started) { + log("GtfsrtService", "Tried to start when already started") + return + } + + if (enableArchiving) { + scope.launch { archiver.start(rawMessages) } + } + + scope.launch { fetch() } + } + + private suspend fun fetch() { + coroutineScope { + types.map { type -> + launch(context = Dispatchers.IO) { + val logger = LogScope("gtfsr $type") + try { + val res = client.get { + url("https://api.opendata.transport.vic.gov.au/opendata/public-transport/gtfs/realtime/v1/${type}") + header("KeyId", Constants.opendataKey) + } + if (!res.status.isSuccess()) { + logger.log("${res.status} | ${res.bodyAsText()}") + } else { + val bytes = res.readRawBytes() + rawMessages.emit(type to bytes) + } + } catch (e: Throwable) { + logger.log("$e") + logger.log(e.stackTraceToString()) + } + } + }.joinAll() + } + + delay(10000) + fetch() + } +} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/gtfsr/RealtimeVehiclePosition.kt b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/RealtimeVehiclePositions.kt similarity index 88% rename from shared/src/commonMain/kotlin/moe/lava/banksia/data/gtfsr/RealtimeVehiclePosition.kt rename to server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/RealtimeVehiclePositions.kt index 979f1f5..4466b91 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/data/gtfsr/RealtimeVehiclePosition.kt +++ b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/RealtimeVehiclePositions.kt @@ -1,7 +1,7 @@ -package moe.lava.banksia.data.gtfsr +package moe.lava.banksia.server.gtfsrt import com.google.transit.realtime.FeedMessage -import moe.lava.banksia.util.Point +import moe.lava.banksia.core.util.Point class RealtimeVehiclePositions(data: FeedMessage) : GtfsRealtime(data) { private val positions = mutableMapOf() diff --git a/shared/src/commonMain/proto/gtfs-realtime.proto b/server/gtfs_rt/src/main/proto/gtfs-realtime.proto similarity index 100% rename from shared/src/commonMain/proto/gtfs-realtime.proto rename to server/gtfs_rt/src/main/proto/gtfs-realtime.proto diff --git a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt index 76ee8ba..dedffe5 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt +++ b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt @@ -15,47 +15,59 @@ import io.ktor.server.routing.routing import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.todayIn -import moe.lava.banksia.Constants -import moe.lava.banksia.di.CommonModules -import moe.lava.banksia.model.atDate -import moe.lava.banksia.room.dao.RouteDao -import moe.lava.banksia.room.dao.StopDao -import moe.lava.banksia.room.dao.StopTimeDao -import moe.lava.banksia.room.dao.VersionMetadataDao +import moe.lava.banksia.core.Constants +import moe.lava.banksia.core.sqld.RouteQueries +import moe.lava.banksia.core.sqld.StopQueries +import moe.lava.banksia.core.sqld.mappers.asModel import moe.lava.banksia.server.di.ServerModules -import moe.lava.banksia.server.gtfs.GtfsHandler -import moe.lava.banksia.server.gtfsr.GtfsrService -import moe.lava.banksia.util.serialise +import moe.lava.banksia.server.gtfsrt.GtfsrtService +import moe.lava.banksia.server.routes.stopTimeRoutes import org.koin.dsl.module -import org.koin.ktor.ext.inject +import org.koin.ktor.ext.get import org.koin.ktor.plugin.Koin -import kotlin.time.Clock fun main() { + if (System.getenv("BANKSIA_PRODUCTION") == "1") Constants.devMode = false + embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module) .start(wait = true) } fun Application.module() { + log.info("devMode: ${Constants.devMode}") install(ContentNegotiation) { json() } install(Koin) { modules(module { single { log } }) - modules(CommonModules, ServerModules) + modules(ServerModules) } @Suppress("KotlinConstantConditions") - if (!Constants.devMode) { - val gtfsr by inject() - launch { gtfsr.start() } - } + launch { get().start(this, !Constants.devMode) } routing { - get("/update") { + stopTimeRoutes() + + if (Constants.devMode) { + get("/fixup") { + call.respondText("received") + get().addParentsToStops() + } + } + get("/manage/fixup") { + val key = call.parameters["key"] + if (key != Constants.updateKey) { + call.respond(HttpStatusCode.Forbidden) + return@get + } + + call.respondText("fixing") + launch(context = Dispatchers.IO) { + get().addParentsToStops() + } + } + get("/manage/update") { val key = call.parameters["key"] if (key != Constants.updateKey) { call.respond(HttpStatusCode.Forbidden) @@ -67,30 +79,14 @@ fun Application.module() { ?: "https://opendata.transport.vic.gov.au/dataset/3f4e292e-7f8a-4ffe-831f-1953be0fe448/resource/${datasetUuid}/download/gtfs.zip" call.respondText("received") launch(context = Dispatchers.IO) { - val handler by inject() - handler.update(datasetUrl) - } - } - - get("/metadata/{type?}") { - val dao by inject() - val type = call.parameters["type"] - if (type == null) { - call.respond(dao.getAll().map { it.asModel() }) - return@get - } - - val data = dao.get(type)?.asModel() - if (data == null) { - call.respond(HttpStatusCode.NotFound) - } else { - call.respond(data) + get().import(datasetUrl) + get().addParentsToStops() } } get("/routes") { val routes = withContext(context = Dispatchers.IO) { - inject().value.getAll() + get().getAll().executeAsList() } val res = routes.map { it.asModel() } call.respond(res) @@ -98,16 +94,17 @@ fun Application.module() { get("/routes/{route_id}") { val routeId = call.parameters["route_id"]!! val route = withContext(context = Dispatchers.IO) { - inject().value.get(routeId) + get().get(routeId).executeAsOneOrNull() } - if (route != null) + if (route != null) { call.respond(route.asModel()) - else + } else { call.respond(HttpStatusCode.NotFound) + } } get("/stops") { val routes = withContext(context = Dispatchers.IO) { - inject().value.getAll() + get().getAll().executeAsList() } val res = routes.map { it.asModel() } call.respond(res) @@ -115,56 +112,26 @@ fun Application.module() { get("/stops/{stop_id}") { val stopId = call.parameters["stop_id"]!! val stop = withContext(context = Dispatchers.IO) { - inject().value.get(stopId) + get().get(stopId).executeAsOneOrNull() } - if (stop != null) + if (stop != null) { call.respond(stop.asModel()) - else + } else { call.respond(HttpStatusCode.NotFound) + } } get("/route_stops/{route_id}") { val routeId = call.parameters["route_id"]!! - val useParent = call.queryParameters["parent"] in listOf("true", "1") + val useParent = call.queryParameters["parent"] !in listOf("false", "0") val stops = withContext(Dispatchers.IO) { - val routeDao by inject() - if (useParent) - routeDao.stopsParent(routeId) - else - routeDao.stops(routeId) + val queries = get() + if (useParent) { + queries.getParentsByRoute(routeId).executeAsList() + } else { + queries.getByRoute(routeId).executeAsList() + } } call.respond(stops.map { it.asModel() }) -// val stops = withContext(Dispatchers.IO) { -// val stopDao by inject() -// val stopTimeDao by inject() -// val tripDao by inject() -// -// tripDao.getByRoute(routeId) -// .map { it.id } -// .let { stopTimeDao.get(it) } -// .flatMap { it.asModel().stopInfos } -// .map { it.stopId } -// .let { stopDao.get(it) } -// .map { it.asModel() } -// } -// call.respond(stops) - - } - get("/stoptimes/by_stop/{stop_id}") { - val stopId = call.parameters["stop_id"]!! - val date = call.queryParameters["date"] - ?.let { LocalDate.parse(it, LocalDate.Formats.ISO) } - ?: Clock.System.todayIn(TimeZone.currentSystemDefault()) - val times = withContext(context = Dispatchers.IO) { - inject().value - .getForStopDated( - stopId, - listOf(date.dayOfWeek).serialise(), - date.toEpochDays().toInt(), - ) - .map { it.asModel().atDate(date) } - .sortedBy { it.departureTime } - } - call.respond(times) } } } diff --git a/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt b/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt new file mode 100644 index 0000000..97892e0 --- /dev/null +++ b/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt @@ -0,0 +1,45 @@ +package moe.lava.banksia.server + +import moe.lava.banksia.core.sqld.BanksiaDatabase +import moe.lava.banksia.core.util.log +import java.security.MessageDigest +import moe.lava.banksia.core.sqld.Stop as DbStop + +class GtfsDataFixer( + private val database: BanksiaDatabase, +) { + fun addParentsToStops() { + val queries = database.stopQueries + val stops = queries.getAllParentless().executeAsList() + stops + .groupBy { it.name.split("/")[0] } + .filter { (_, stops) -> stops.size > 1 } + .forEach { (name, stops) -> + val avgLat = stops.map { it.lat }.average() + val avgLng = stops.map { it.lng }.average() + val hash = name.sha256().substring(0, 7) + val parentId = "bsia:df1:$hash" + val parent = DbStop( + id = parentId, + name = name, + lat = avgLat, + lng = avgLng, + parent = null, + hasWheelChairBoarding = if (stops.all { it.hasWheelChairBoarding == 1L }) 1L else 0L, + level = "", + platformCode = "", + ) + log("datafixer", "inserting ${parentId} for ${stops.size} children") + queries.transaction { + queries.insert(parent) + queries.updateParents(parentId, stops.map { it.id }) + } + } + } +} + +private fun String.sha256() = + MessageDigest + .getInstance("SHA-256") + .digest(this.toByteArray()) + .joinToString("") { "%02x".format(it) } diff --git a/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt b/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt new file mode 100644 index 0000000..84fae70 --- /dev/null +++ b/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt @@ -0,0 +1,115 @@ +package moe.lava.banksia.server + +import io.ktor.util.logging.Logger +import moe.lava.banksia.core.model.Route +import moe.lava.banksia.core.model.Service +import moe.lava.banksia.core.model.ServiceException +import moe.lava.banksia.core.model.Shape +import moe.lava.banksia.core.model.Stop +import moe.lava.banksia.core.model.Trip +import moe.lava.banksia.core.sqld.DatabaseManager +import moe.lava.banksia.core.sqld.mappers.asDb +import moe.lava.banksia.server.gtfs.GtfsData +import moe.lava.banksia.server.gtfs.GtfsParser +import kotlin.time.Clock +import moe.lava.banksia.core.sqld.BanksiaDatabase as Database + +class GtfsImporter( + private val parser: GtfsParser, + private val dbm: DatabaseManager, + private val log: Logger, +) { + suspend fun import(url: String, date: Long = Clock.System.now().epochSeconds) { + val (database, close) = dbm.makeAlt() + + parser.update(url).collect { chunk -> + when (chunk) { + is GtfsData.RouteChunk -> database.addRoutes(chunk.routes) + is GtfsData.ServiceChunk -> database.addServices(chunk.services) + is GtfsData.ServiceExceptionChunk -> database.addServiceExceptions(chunk.exceptions) + is GtfsData.ShapeChunk -> database.addShapes(chunk.shapes) + is GtfsData.StopChunk -> database.addStops(chunk.stops) + is GtfsData.TripChunk -> database.addTrips(chunk.trips) + } + } + + close() + dbm.swap() + } + + private fun Database.addRoutes(routes: List) { + log.info("inserting routes...") + routeQueries.transaction { + routes.forEach { + routeQueries.insert(it.asDb()) + } + } + log.info("done") + } + + private fun Database.addServices(services: List) { + log.info("inserting services...") + serviceQueries.transaction { + services.forEach { + serviceQueries.insert(it.asDb()) + } + } + log.info("done") + } + + private fun Database.addServiceExceptions(exceptions: List) { + log.info("inserting exceptions...") + serviceExceptionQueries.transaction { + exceptions.forEach { + serviceExceptionQueries.insert(it.asDb()) + } + } + log.info("done") + } + + private fun Database.addShapes(shapes: List) { + log.info("inserting shapes...") + shapeQueries.transaction { + shapes.forEach { + shapeQueries.insert(it.asDb()) + } + } + log.info("done") + } + + private fun Database.addStops(stops: List) { + log.info("inserting stops...") + stops + .groupBy { it.id } + .forEach { (id, gstops) -> + if (gstops.size > 1) { + if (gstops.withIndex().any { (i, stop) -> i != 0 && stop != gstops[i - 1] }) { + gstops.forEach { + log.warn("duplicate $id: $it") + } + } + } + } + + stopQueries.transaction { + stops.forEach { + stopQueries.insert(it.asDb()) + } + } + log.info("done") + } + + private fun Database.addTrips(trips: List) { + log.info("inserting ${trips.size} trips...") + transaction { + trips.forEach { trip -> + stoppingPatternQueries.insert(trip.pattern.asDb()) + trip.pattern.stoptimes.forEach { stoptime -> + stopTimeQueries.insert(stoptime.asDb()) + } + tripQueries.insert(trip.asDb()) + } + } + log.info("done") + } +} diff --git a/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt b/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt index c7b650c..b2593b3 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt +++ b/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt @@ -1,13 +1,22 @@ package moe.lava.banksia.server.di import io.ktor.client.HttpClient -import moe.lava.banksia.server.gtfs.GtfsHandler -import moe.lava.banksia.server.gtfsr.GtfsrService +import moe.lava.banksia.core.data.dataDiModule +import moe.lava.banksia.server.GtfsDataFixer +import moe.lava.banksia.server.GtfsImporter +import moe.lava.banksia.server.gtfs.GtfsParser +import moe.lava.banksia.server.gtfsrt.GtfsrtService +import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val ServerModules = module { + includes(dataDiModule) + single { HttpClient() } - singleOf(::GtfsHandler) - singleOf(::GtfsrService) + singleOf(::GtfsParser) + singleOf(::GtfsrtService) + + factoryOf(::GtfsDataFixer) + factoryOf(::GtfsImporter) } diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt deleted file mode 100644 index 28d50af..0000000 --- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt +++ /dev/null @@ -1,336 +0,0 @@ -package moe.lava.banksia.server.gtfs - -import com.lightningkite.kotlinx.serialization.csv.CsvFormat -import com.lightningkite.kotlinx.serialization.csv.StringDeferringConfig -import io.ktor.client.HttpClient -import io.ktor.client.request.prepareRequest -import io.ktor.client.request.url -import io.ktor.client.statement.bodyAsChannel -import io.ktor.util.cio.writeChannel -import io.ktor.util.logging.Logger -import io.ktor.utils.io.copyAndClose -import kotlinx.datetime.DayOfWeek -import kotlinx.datetime.LocalDate -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.serializer -import moe.lava.banksia.Constants -import moe.lava.banksia.model.Route -import moe.lava.banksia.model.Service -import moe.lava.banksia.model.Shape -import moe.lava.banksia.model.Stop -import moe.lava.banksia.model.StopTime -import moe.lava.banksia.model.Trip -import moe.lava.banksia.room.Database -import moe.lava.banksia.room.converter.RouteTypeConverter -import moe.lava.banksia.room.entity.asEntity -import moe.lava.banksia.server.gtfs.structures.GtfsRoute -import moe.lava.banksia.server.gtfs.structures.GtfsService -import moe.lava.banksia.server.gtfs.structures.GtfsShape -import moe.lava.banksia.server.gtfs.structures.GtfsStop -import moe.lava.banksia.server.gtfs.structures.GtfsStopTime -import moe.lava.banksia.server.gtfs.structures.GtfsTrip -import moe.lava.banksia.util.Point -import java.io.File -import java.util.zip.ZipFile -import kotlin.time.Clock -import kotlin.time.ExperimentalTime - -class GtfsHandler( - private val log: Logger, - private val client: HttpClient, - private val db: Database, -) { - private val csv = CsvFormat(StringDeferringConfig(EmptySerializersModule())) - private val datasetPath = File("/tmp/banksia", "dataset.zip") - - @OptIn(ExperimentalTime::class) - suspend fun update(datasetUrl: String, date: Long? = null) { - val parentDir = datasetPath.parentFile - @Suppress("SimplifyBooleanWithConstants", "KotlinConstantConditions") - if (parentDir.exists() && !Constants.devMode) - parentDir.deleteRecursively() - - parentDir.mkdirs() - - log.info("fetching..") - client.prepareRequest { - url(datasetUrl) - }.execute { resp -> - if (!datasetPath.exists()) - resp.bodyAsChannel().copyAndClose(datasetPath.writeChannel()) - log.info("fetched!") - } - - log.info("extracting...") - @Suppress("KotlinConstantConditions") - val files = if (Constants.devMode) { - datasetPath.parentFile - .listFiles { it.isDirectory } - .flatMap { d -> d.listFiles { f -> f.extension == "txt" }.toList() } - .ifEmpty { extractAll(datasetPath) } - .filter { it.parentFile.name == "2" } - } else { - extractAll(datasetPath) - } - - addRoutes(files) - addStops(files) - addShapes(files) - val services = addServices(files) - val trips = addTrips(files, services.associateBy { it.id }) - addStopTimes(files, trips.associateBy { it.id }) - - updateMetadata(date ?: Clock.System.now().epochSeconds) - - @Suppress("KotlinConstantConditions") - if (!Constants.devMode) { - parentDir.deleteRecursively() - } - - log.info("done!") - } - - private suspend fun updateMetadata(date: Long) { - val dao = db.versionMetadataDao - log.info("updating metadata...") - dao.update(date, listOf("routes", "stops", "shapes", "trips", "stop_times")) - } - - private suspend fun addRoutes(files: List) { - val dao = db.routeDao - log.info("parsing routes...") - val routes = files - .filter { it.name == "routes.txt" } - .flatMap { fd -> parseRoutes(fd) } - - log.info("inserting routes...") - dao.deleteAll() - dao.insertAll(*routes.map { it.asEntity() }.toTypedArray()) - } - - private fun parseRoutes(fd: File) = - fd.parseCsv() - .map { with(it) { - Route( - id = route_id, - type = RouteTypeConverter.from(fd.parentFile.name.toInt()), - number = route_short_name, - name = route_long_name, - ) - } } - - private suspend fun addShapes(files: List) { - val dao = db.shapeDao - log.info("parsing shapes...") - val shapes = files - .filter { it.name == "shapes.txt" } - .flatMap { fd -> parseShapes(fd) } - - log.info("inserting shapes...") - dao.deleteAll() - dao.insertAll(*shapes.map { it.asEntity() }.toTypedArray()) - } - - private fun parseShapes(fd: File) = - fd.parseCsv() - .groupBy { it.shape_id } - .map { (id, group) -> - val points = group - .sortedBy { it.shape_pt_sequence } - .map { Point(it.shape_pt_lat, it.shape_pt_lon) } - - Shape(id, points) - } - - private suspend fun addStops(files: List) { - val dao = db.stopDao - log.info("parsing stops...") - val stops = files - .filter { it.name == "stops.txt" } - .flatMap { fd -> parseStops(fd) } - - log.info("inserting stops...") - dao.deleteAll() - stops - .groupBy { it.id } - .forEach { (id, gstops) -> - if (gstops.size > 1) { - if (gstops.withIndex().any { (i, stop) -> i != 0 && stop != gstops[i - 1] }) { - gstops.forEach { - log.info("duplicate $id: $it") - } - } - } - } - dao.insertOrReplaceAll(*stops.map { it.asEntity() }.toTypedArray()) - } - - private fun parseStops(fd: File) = - fd.parseCsv() - .map { with(it) { - Stop( - id = stop_id, - name = stop_name, - pos = Point(stop_lat, stop_lon), - parent = parent_station, - hasWheelChairBoarding = wheelchair_boarding == "1", - level = level_id, - platformCode = platform_code, - ) - } } - - private suspend fun addStopTimes(files: List, trips: Map) { - val dao = db.stopTimeDao - dao.deleteAll() - log.info("parsing stop times...") - files - .filter { it.name == "stop_times.txt" } - .forEach { fd -> - log.info("parsing stop times for ${fd.parent}...") - parseStopTimes(fd, trips) { seq -> - seq.chunked(1000000) - .forEach { queue -> - log.info("converting stop times (${queue.size}) for ${fd.parent}...") - val conv = queue.map { it.asEntity() }.toTypedArray() - log.info("inserting stop times (${conv.size}) for ${fd.parent}...") - dao.insertOrReplaceAll(*conv) - } - } - } - } - - private inline fun parseStopTimes(fd: File, trips: Map, block: (Sequence) -> Unit) = - fd.parseCsvSequence { seq -> - seq - .map { with(it) { - StopTime( - tripId = trip_id, - stopId = stop_id, - arrivalTime = GtfsStopTime.parseGtfsTime(arrival_time), - departureTime = GtfsStopTime.parseGtfsTime(departure_time), - headsign = stop_headsign.ifEmpty { trips[trip_id]!!.tripHeadsign }, - pickupType = pickup_type, - dropOffType = drop_off_type, - ) - } } - .let { block(it) } - } - - private suspend fun addServices(files: List): List { - val dao = db.serviceDao - log.info("parsing services...") - val services = files - .filter { it.name == "calendar.txt" } - .flatMap { fd -> parseServices(fd) } - - log.info("inserting services...") - dao.deleteAll() - dao.insertOrReplaceAll(*services.map { it.asEntity() }.toTypedArray()) - - return services - } - - private fun parseServices(fd: File) = - fd.parseCsv() - .map { with(it) { - val days = buildList { - if (monday == 1) add(DayOfWeek.MONDAY) - if (tuesday == 1) add(DayOfWeek.TUESDAY) - if (wednesday == 1) add(DayOfWeek.WEDNESDAY) - if (thursday == 1) add(DayOfWeek.THURSDAY) - if (friday == 1) add(DayOfWeek.FRIDAY) - if (saturday == 1) add(DayOfWeek.SATURDAY) - if (sunday == 1) add(DayOfWeek.SUNDAY) - } - Service( - id = service_id, - days = days, - start = LocalDate.parse(start_date, LocalDate.Formats.ISO_BASIC), - end = LocalDate.parse(end_date, LocalDate.Formats.ISO_BASIC), - ) - } } - - private suspend fun addTrips(files: List, services: Map): List { - val dao = db.tripDao - log.info("parsing trips...") - val trips = files - .filter { it.name == "trips.txt" } - .flatMap { fd -> parseTrips(fd, services) } - - log.info("inserting trips...") - dao.deleteAll() - dao.insertOrReplaceAll(*trips.map { it.asEntity() }.toTypedArray()) - - return trips - } - - private fun parseTrips(fd: File, services: Map) = - fd.parseCsv() - .map { with(it) { - Trip( - id = trip_id, - routeId = route_id, - service = services[service_id]!!, - shapeId = shape_id.ifEmpty { null }, - tripHeadsign = trip_headsign, - directionId = direction_id, - blockId = block_id, - wheelchairAccessible = wheelchair_accessible, - ) - } } - - private fun extract(fd: File): List { - val outputs = mutableListOf() - ZipFile(fd).use { zip -> - for (entry in zip.entries()) { - zip.getInputStream(entry).use { input -> - val out = File(fd.parentFile, entry.name) - out.parentFile.mkdirs() - out.outputStream().use { output -> - input.copyTo(output) - } - outputs.add(out) - } - } - } - return outputs - } - - private fun extractAll(fd: File) = extract(fd).flatMap(::extract) - - private inline fun File.parseCsv(): List = this - .readText() - .replace("\uFEFF", "") // remove bom - .replace("\r\n", "\n") // crlf -> lf - .let { csv.decodeFromString(it) } - - private inline fun File.parseCsvSequence(block: (Sequence) -> Unit) = this - .bufferedReader() - .use { reader -> - val iter = object : CharIterator() { - var next: Char? = null - override fun nextChar(): Char { - if (!hasNext()) { - throw NoSuchElementException() - } - val ret = next!! - next = null - return ret - } - override fun hasNext(): Boolean { - if (next == null) { - do { - next = null - val new = reader.read() - if (new != -1) { - next = new.toChar() - } - } while (next == '\uFEFF' || next == '\r') - } - return next != null - } - } - block(csv.decodeToSequence(iter, csv.serializersModule.serializer())) - } -} diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfsr/GtfsrService.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfsr/GtfsrService.kt deleted file mode 100644 index 5a0b1dc..0000000 --- a/server/src/main/kotlin/moe/lava/banksia/server/gtfsr/GtfsrService.kt +++ /dev/null @@ -1,164 +0,0 @@ -package moe.lava.banksia.server.gtfsr - -import com.google.transit.realtime.FeedMessage -import io.ktor.client.HttpClient -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.client.request.url -import io.ktor.client.statement.bodyAsText -import io.ktor.client.statement.readRawBytes -import io.ktor.http.isSuccess -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import moe.lava.banksia.Constants -import moe.lava.banksia.util.LogScope -import moe.lava.banksia.util.log -import java.io.File -import java.time.Instant -import java.time.ZoneId - -private const val BASE_DIR = "./data/gtfsr-archive/" - -class GtfsrService(private val client: HttpClient) { - private var started = false - private val latest = mutableMapOf() - - fun latestFor(type: String) = latest[type] - - private val iFlow = MutableSharedFlow>() - val flow = iFlow.asSharedFlow() - - companion object { - val types = arrayOf( - "metro/trip-updates", - "metro/vehicle-positions", - "metro/service-alerts", - "tram/trip-updates", - "tram/vehicle-positions", - "tram/service-alerts", - "bus/trip-updates", - "bus/vehicle-positions", - "vline/trip-updates", - "vline/vehicle-positions", - ) - } - - suspend fun start() { - if (started) { - log("GtfsrService", "Tried to start when already started") - return - } - started = true - coroutineScope { - launch { compressJob() } - - while (true) { - val results = mutableMapOf() - types.map { type -> - launch(context = Dispatchers.IO) { - val logger = LogScope("gtfsr $type") - try { - val res = client.get { - url("https://api.opendata.transport.vic.gov.au/opendata/public-transport/gtfs/realtime/v1/${type}") - header("KeyId", Constants.opendataKey) - } - if (!res.status.isSuccess()) { - logger.log("${res.status} | ${res.bodyAsText()}") - } else { - results[type] = res.readRawBytes() - } - } catch (e: Throwable) { - logger.log("$e") - logger.log(e.stackTraceToString()) - } - } - }.joinAll() - - results.forEach { (type, data) -> - val dec = try { - FeedMessage.ADAPTER.decode(data) - } catch (e: Throwable) { - log("gtfsr $type", "Failed to parse proto: $e") - return@forEach - } - val timestamp = dec.header_.timestamp - ?: return@forEach log("gtfsr $type", "Failed to read proto timestamp") - - val time = Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault()) - - val base = File(BASE_DIR, type) - val previousParent = File(base, "${time.year}-${((time.dayOfYear - 1) / 7).toString().padStart(2, '0')}") - val currentParent = File(base, "${time.year}-${((time.dayOfYear - 1) / 7 + 1).toString().padStart(2, '0')}") - val target = File(currentParent, "${timestamp}.proto") - - if (previousParent.isDirectory) { - enqueueCompression(previousParent) - } - - if (!target.exists()) { - try { - if (!target.parentFile.isDirectory) { - target.parentFile.mkdirs() - } - target.writeBytes(data) - } catch (e: Throwable) { - log("gtfsr $type", "Failed to write ${target}: $e") - } - } - } - delay(10000) - } - } - } - - private val cqueue = mutableSetOf() - private val ignore = mutableSetOf() - private val cmut = Mutex() - private suspend fun enqueueCompression(fd: File) { - cmut.withLock { cqueue.add(fd) } - } - - private suspend fun compressJob() { - while(true) { - while(true) { - val next = cmut.withLock { cqueue.firstOrNull() } - ?: break - if (!next.isDirectory) { - cmut.withLock { cqueue.remove(next) } - continue - } - if (next in ignore) continue - - withContext(Dispatchers.IO) { - val proc = ProcessBuilder( - "tar", "-acf", - "${next.absolutePath}.tar.zst", - next.absolutePath - ).start() - val exitCode = proc.waitFor() - if (exitCode == 0) { - if (next.deleteRecursively()) { - cmut.withLock { cqueue.remove(next) } - } else { - log("CompressJob", "Failed to delete $next") - ignore.add(next) - } - } else { - val msg = proc.errorStream.readAllBytes().decodeToString() - log("CompressJob", "Failed to delete $next (exit code $exitCode") - log("CompressJob", msg) - } - } - } - delay(30000) - } - } -} diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml index de5d8bf..6519371 100644 --- a/server/src/main/resources/logback.xml +++ b/server/src/main/resources/logback.xml @@ -14,7 +14,7 @@ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - + diff --git a/settings.gradle.kts b/settings.gradle.kts index 4688423..bdb499e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,9 +32,13 @@ dependencyResolutionManagement { } include(":androidApp") -include(":client") include(":server") -include(":shared") +include(":server:gtfs") +include(":server:gtfs_rt") +include(":core") +include(":core:data") +include(":core:stoptime") +include(":core:sqld") include(":ui") include(":ui:maps") include(":ui:shared") diff --git a/shared/schemas/moe.lava.banksia.room.Database/1.json b/shared/schemas/moe.lava.banksia.room.Database/1.json deleted file mode 100644 index 037062e..0000000 --- a/shared/schemas/moe.lava.banksia.room.Database/1.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "e536f5a9b1408377bcc449195169648c", - "entities": [ - { - "tableName": "Route", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Shape", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "path", - "columnName": "path", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e536f5a9b1408377bcc449195169648c')" - ] - } -} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/2.json b/shared/schemas/moe.lava.banksia.room.Database/2.json deleted file mode 100644 index 04a14e3..0000000 --- a/shared/schemas/moe.lava.banksia.room.Database/2.json +++ /dev/null @@ -1,315 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 2, - "identityHash": "83ece554400bb035c267dc2414c23293", - "entities": [ - { - "tableName": "Route", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Shape", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "path", - "columnName": "path", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Stop", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lat", - "columnName": "lat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "lng", - "columnName": "lng", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "parent", - "columnName": "parent", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "hasWheelChairBoarding", - "columnName": "hasWheelChairBoarding", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "level", - "columnName": "level", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformCode", - "columnName": "platformCode", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Stop_parent", - "unique": false, - "columnNames": [ - "parent" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)" - } - ] - }, - { - "tableName": "StopTime", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tripId", - "columnName": "tripId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "stopId", - "columnName": "stopId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "arrivalTime", - "columnName": "arrivalTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "departureTime", - "columnName": "departureTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "headsign", - "columnName": "headsign", - "affinity": "TEXT" - }, - { - "fieldPath": "pickupType", - "columnName": "pickupType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dropOffType", - "columnName": "dropOffType", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tripId", - "stopId" - ] - }, - "foreignKeys": [ - { - "table": "Trip", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "tripId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Stop", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "stopId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Trip", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "routeId", - "columnName": "routeId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "serviceId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shapeId", - "columnName": "shapeId", - "affinity": "TEXT" - }, - { - "fieldPath": "tripHeadsign", - "columnName": "tripHeadsign", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "directionId", - "columnName": "directionId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "blockId", - "columnName": "blockId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "wheelchairAccessible", - "columnName": "wheelchairAccessible", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Trip_routeId", - "unique": false, - "columnNames": [ - "routeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)" - } - ], - "foreignKeys": [ - { - "table": "Route", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "routeId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Shape", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "shapeId" - ], - "referencedColumns": [ - "id" - ] - } - ] - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '83ece554400bb035c267dc2414c23293')" - ] - } -} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/3.json b/shared/schemas/moe.lava.banksia.room.Database/3.json deleted file mode 100644 index e769926..0000000 --- a/shared/schemas/moe.lava.banksia.room.Database/3.json +++ /dev/null @@ -1,339 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 3, - "identityHash": "5a7252ab3bcae4d0d0950024b19ba002", - "entities": [ - { - "tableName": "Route", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Shape", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "path", - "columnName": "path", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Stop", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lat", - "columnName": "lat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "lng", - "columnName": "lng", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "parent", - "columnName": "parent", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "hasWheelChairBoarding", - "columnName": "hasWheelChairBoarding", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "level", - "columnName": "level", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformCode", - "columnName": "platformCode", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Stop_parent", - "unique": false, - "columnNames": [ - "parent" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)" - } - ] - }, - { - "tableName": "StopTime", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tripId", - "columnName": "tripId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "stopId", - "columnName": "stopId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "arrivalTime", - "columnName": "arrivalTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "departureTime", - "columnName": "departureTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "headsign", - "columnName": "headsign", - "affinity": "TEXT" - }, - { - "fieldPath": "pickupType", - "columnName": "pickupType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dropOffType", - "columnName": "dropOffType", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tripId", - "stopId" - ] - }, - "foreignKeys": [ - { - "table": "Trip", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "tripId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Stop", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "stopId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Trip", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "routeId", - "columnName": "routeId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "serviceId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shapeId", - "columnName": "shapeId", - "affinity": "TEXT" - }, - { - "fieldPath": "tripHeadsign", - "columnName": "tripHeadsign", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "directionId", - "columnName": "directionId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "blockId", - "columnName": "blockId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "wheelchairAccessible", - "columnName": "wheelchairAccessible", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Trip_routeId", - "unique": false, - "columnNames": [ - "routeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)" - } - ], - "foreignKeys": [ - { - "table": "Route", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "routeId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Shape", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "shapeId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "VersionMetadata", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type" - ] - } - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5a7252ab3bcae4d0d0950024b19ba002')" - ] - } -} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/4.json b/shared/schemas/moe.lava.banksia.room.Database/4.json deleted file mode 100644 index 783b3ee..0000000 --- a/shared/schemas/moe.lava.banksia.room.Database/4.json +++ /dev/null @@ -1,368 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 4, - "identityHash": "4426fd2ccc826d9d9d9021546b105850", - "entities": [ - { - "tableName": "Route", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Shape", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "path", - "columnName": "path", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Stop", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lat", - "columnName": "lat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "lng", - "columnName": "lng", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "parent", - "columnName": "parent", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "hasWheelChairBoarding", - "columnName": "hasWheelChairBoarding", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "level", - "columnName": "level", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformCode", - "columnName": "platformCode", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Stop_parent", - "unique": false, - "columnNames": [ - "parent" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)" - } - ] - }, - { - "tableName": "StopTime", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tripId", - "columnName": "tripId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "stopId", - "columnName": "stopId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "arrivalTime", - "columnName": "arrivalTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "departureTime", - "columnName": "departureTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "headsign", - "columnName": "headsign", - "affinity": "TEXT" - }, - { - "fieldPath": "pickupType", - "columnName": "pickupType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dropOffType", - "columnName": "dropOffType", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tripId", - "stopId" - ] - }, - "indices": [ - { - "name": "index_StopTime_tripId", - "unique": true, - "columnNames": [ - "tripId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)" - }, - { - "name": "index_StopTime_stopId", - "unique": true, - "columnNames": [ - "stopId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_StopTime_stopId` ON `${TABLE_NAME}` (`stopId`)" - } - ], - "foreignKeys": [ - { - "table": "Trip", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "tripId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Stop", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "stopId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Trip", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "routeId", - "columnName": "routeId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "serviceId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shapeId", - "columnName": "shapeId", - "affinity": "TEXT" - }, - { - "fieldPath": "tripHeadsign", - "columnName": "tripHeadsign", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "directionId", - "columnName": "directionId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "blockId", - "columnName": "blockId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "wheelchairAccessible", - "columnName": "wheelchairAccessible", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Trip_shapeId", - "unique": false, - "columnNames": [ - "shapeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_shapeId` ON `${TABLE_NAME}` (`shapeId`)" - }, - { - "name": "index_Trip_routeId", - "unique": false, - "columnNames": [ - "routeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)" - } - ], - "foreignKeys": [ - { - "table": "Route", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "routeId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Shape", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "shapeId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "VersionMetadata", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type" - ] - } - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4426fd2ccc826d9d9d9021546b105850')" - ] - } -} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/5.json b/shared/schemas/moe.lava.banksia.room.Database/5.json deleted file mode 100644 index c4a786d..0000000 --- a/shared/schemas/moe.lava.banksia.room.Database/5.json +++ /dev/null @@ -1,368 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 5, - "identityHash": "4426fd2ccc826d9d9d9021546b105850", - "entities": [ - { - "tableName": "Route", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Shape", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "path", - "columnName": "path", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Stop", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lat", - "columnName": "lat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "lng", - "columnName": "lng", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "parent", - "columnName": "parent", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "hasWheelChairBoarding", - "columnName": "hasWheelChairBoarding", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "level", - "columnName": "level", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformCode", - "columnName": "platformCode", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Stop_parent", - "unique": false, - "columnNames": [ - "parent" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)" - } - ] - }, - { - "tableName": "StopTime", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tripId", - "columnName": "tripId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "stopId", - "columnName": "stopId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "arrivalTime", - "columnName": "arrivalTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "departureTime", - "columnName": "departureTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "headsign", - "columnName": "headsign", - "affinity": "TEXT" - }, - { - "fieldPath": "pickupType", - "columnName": "pickupType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dropOffType", - "columnName": "dropOffType", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tripId", - "stopId" - ] - }, - "indices": [ - { - "name": "index_StopTime_tripId", - "unique": true, - "columnNames": [ - "tripId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)" - }, - { - "name": "index_StopTime_stopId", - "unique": true, - "columnNames": [ - "stopId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_StopTime_stopId` ON `${TABLE_NAME}` (`stopId`)" - } - ], - "foreignKeys": [ - { - "table": "Trip", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "tripId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Stop", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "stopId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Trip", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "routeId", - "columnName": "routeId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "serviceId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shapeId", - "columnName": "shapeId", - "affinity": "TEXT" - }, - { - "fieldPath": "tripHeadsign", - "columnName": "tripHeadsign", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "directionId", - "columnName": "directionId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "blockId", - "columnName": "blockId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "wheelchairAccessible", - "columnName": "wheelchairAccessible", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Trip_shapeId", - "unique": false, - "columnNames": [ - "shapeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_shapeId` ON `${TABLE_NAME}` (`shapeId`)" - }, - { - "name": "index_Trip_routeId", - "unique": false, - "columnNames": [ - "routeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)" - } - ], - "foreignKeys": [ - { - "table": "Route", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "routeId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Shape", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "shapeId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "VersionMetadata", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type" - ] - } - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4426fd2ccc826d9d9d9021546b105850')" - ] - } -} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/6.json b/shared/schemas/moe.lava.banksia.room.Database/6.json deleted file mode 100644 index 5ab26dc..0000000 --- a/shared/schemas/moe.lava.banksia.room.Database/6.json +++ /dev/null @@ -1,368 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 6, - "identityHash": "5f52de4cc0ddbcf02a0d8be4cf4d4cfd", - "entities": [ - { - "tableName": "Route", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Shape", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "path", - "columnName": "path", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Stop", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lat", - "columnName": "lat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "lng", - "columnName": "lng", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "parent", - "columnName": "parent", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "hasWheelChairBoarding", - "columnName": "hasWheelChairBoarding", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "level", - "columnName": "level", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformCode", - "columnName": "platformCode", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Stop_parent", - "unique": false, - "columnNames": [ - "parent" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)" - } - ] - }, - { - "tableName": "StopTime", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tripId", - "columnName": "tripId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "stopId", - "columnName": "stopId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "arrivalTime", - "columnName": "arrivalTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "departureTime", - "columnName": "departureTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "headsign", - "columnName": "headsign", - "affinity": "TEXT" - }, - { - "fieldPath": "pickupType", - "columnName": "pickupType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dropOffType", - "columnName": "dropOffType", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tripId", - "stopId" - ] - }, - "indices": [ - { - "name": "index_StopTime_tripId", - "unique": false, - "columnNames": [ - "tripId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)" - }, - { - "name": "index_StopTime_stopId", - "unique": false, - "columnNames": [ - "stopId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_stopId` ON `${TABLE_NAME}` (`stopId`)" - } - ], - "foreignKeys": [ - { - "table": "Trip", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "tripId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Stop", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "stopId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Trip", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "routeId", - "columnName": "routeId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "serviceId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shapeId", - "columnName": "shapeId", - "affinity": "TEXT" - }, - { - "fieldPath": "tripHeadsign", - "columnName": "tripHeadsign", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "directionId", - "columnName": "directionId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "blockId", - "columnName": "blockId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "wheelchairAccessible", - "columnName": "wheelchairAccessible", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Trip_shapeId", - "unique": false, - "columnNames": [ - "shapeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_shapeId` ON `${TABLE_NAME}` (`shapeId`)" - }, - { - "name": "index_Trip_routeId", - "unique": false, - "columnNames": [ - "routeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)" - } - ], - "foreignKeys": [ - { - "table": "Route", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "routeId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Shape", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "shapeId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "VersionMetadata", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type" - ] - } - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5f52de4cc0ddbcf02a0d8be4cf4d4cfd')" - ] - } -} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/7.json b/shared/schemas/moe.lava.banksia.room.Database/7.json deleted file mode 100644 index d4c62b2..0000000 --- a/shared/schemas/moe.lava.banksia.room.Database/7.json +++ /dev/null @@ -1,415 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 7, - "identityHash": "15c94df0a62438ff28d451c074c94c59", - "entities": [ - { - "tableName": "Route", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Service", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `days` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "days", - "columnName": "days", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "start", - "columnName": "start", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "end", - "columnName": "end", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Service_days", - "unique": false, - "columnNames": [ - "days" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Service_days` ON `${TABLE_NAME}` (`days`)" - } - ] - }, - { - "tableName": "Shape", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "path", - "columnName": "path", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Stop", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lat", - "columnName": "lat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "lng", - "columnName": "lng", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "parent", - "columnName": "parent", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "hasWheelChairBoarding", - "columnName": "hasWheelChairBoarding", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "level", - "columnName": "level", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformCode", - "columnName": "platformCode", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Stop_parent", - "unique": false, - "columnNames": [ - "parent" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)" - } - ] - }, - { - "tableName": "StopTime", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tripId", - "columnName": "tripId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "stopId", - "columnName": "stopId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "arrivalTime", - "columnName": "arrivalTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "departureTime", - "columnName": "departureTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "headsign", - "columnName": "headsign", - "affinity": "TEXT" - }, - { - "fieldPath": "pickupType", - "columnName": "pickupType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dropOffType", - "columnName": "dropOffType", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tripId", - "stopId" - ] - }, - "indices": [ - { - "name": "index_StopTime_tripId", - "unique": false, - "columnNames": [ - "tripId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)" - }, - { - "name": "index_StopTime_stopId", - "unique": false, - "columnNames": [ - "stopId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_stopId` ON `${TABLE_NAME}` (`stopId`)" - } - ], - "foreignKeys": [ - { - "table": "Trip", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "tripId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Stop", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "stopId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Trip", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "routeId", - "columnName": "routeId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "serviceId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shapeId", - "columnName": "shapeId", - "affinity": "TEXT" - }, - { - "fieldPath": "tripHeadsign", - "columnName": "tripHeadsign", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "directionId", - "columnName": "directionId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "blockId", - "columnName": "blockId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "wheelchairAccessible", - "columnName": "wheelchairAccessible", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Trip_shapeId", - "unique": false, - "columnNames": [ - "shapeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_shapeId` ON `${TABLE_NAME}` (`shapeId`)" - }, - { - "name": "index_Trip_routeId", - "unique": false, - "columnNames": [ - "routeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)" - } - ], - "foreignKeys": [ - { - "table": "Route", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "routeId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Shape", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "shapeId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "VersionMetadata", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type" - ] - } - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '15c94df0a62438ff28d451c074c94c59')" - ] - } -} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/8.json b/shared/schemas/moe.lava.banksia.room.Database/8.json deleted file mode 100644 index 9240dd5..0000000 --- a/shared/schemas/moe.lava.banksia.room.Database/8.json +++ /dev/null @@ -1,426 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 8, - "identityHash": "6e0f07bf1af88b2e37b5ad7c38a3fb2a", - "entities": [ - { - "tableName": "Route", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Service", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `days` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "days", - "columnName": "days", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "start", - "columnName": "start", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "end", - "columnName": "end", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Service_days", - "unique": false, - "columnNames": [ - "days" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Service_days` ON `${TABLE_NAME}` (`days`)" - } - ] - }, - { - "tableName": "Shape", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "path", - "columnName": "path", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Stop", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lat", - "columnName": "lat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "lng", - "columnName": "lng", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "parent", - "columnName": "parent", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "hasWheelChairBoarding", - "columnName": "hasWheelChairBoarding", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "level", - "columnName": "level", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformCode", - "columnName": "platformCode", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Stop_parent", - "unique": false, - "columnNames": [ - "parent" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)" - } - ] - }, - { - "tableName": "StopTime", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tripId", - "columnName": "tripId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "stopId", - "columnName": "stopId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "arrivalTime", - "columnName": "arrivalTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "departureTime", - "columnName": "departureTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "headsign", - "columnName": "headsign", - "affinity": "TEXT" - }, - { - "fieldPath": "pickupType", - "columnName": "pickupType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dropOffType", - "columnName": "dropOffType", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tripId", - "stopId" - ] - }, - "indices": [ - { - "name": "index_StopTime_tripId", - "unique": false, - "columnNames": [ - "tripId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)" - }, - { - "name": "index_StopTime_stopId", - "unique": false, - "columnNames": [ - "stopId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_stopId` ON `${TABLE_NAME}` (`stopId`)" - } - ], - "foreignKeys": [ - { - "table": "Trip", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "tripId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Stop", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "stopId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Trip", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serviceId`) REFERENCES `Service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "routeId", - "columnName": "routeId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "serviceId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shapeId", - "columnName": "shapeId", - "affinity": "TEXT" - }, - { - "fieldPath": "tripHeadsign", - "columnName": "tripHeadsign", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "directionId", - "columnName": "directionId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "blockId", - "columnName": "blockId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "wheelchairAccessible", - "columnName": "wheelchairAccessible", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Trip_shapeId", - "unique": false, - "columnNames": [ - "shapeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_shapeId` ON `${TABLE_NAME}` (`shapeId`)" - }, - { - "name": "index_Trip_routeId", - "unique": false, - "columnNames": [ - "routeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)" - } - ], - "foreignKeys": [ - { - "table": "Route", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "routeId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Service", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "serviceId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Shape", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "shapeId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "VersionMetadata", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type" - ] - } - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e0f07bf1af88b2e37b5ad7c38a3fb2a')" - ] - } -} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/9.json b/shared/schemas/moe.lava.banksia.room.Database/9.json deleted file mode 100644 index 2359dbd..0000000 --- a/shared/schemas/moe.lava.banksia.room.Database/9.json +++ /dev/null @@ -1,426 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 9, - "identityHash": "6e0f07bf1af88b2e37b5ad7c38a3fb2a", - "entities": [ - { - "tableName": "Route", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "number", - "columnName": "number", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Service", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `days` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "days", - "columnName": "days", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "start", - "columnName": "start", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "end", - "columnName": "end", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Service_days", - "unique": false, - "columnNames": [ - "days" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Service_days` ON `${TABLE_NAME}` (`days`)" - } - ] - }, - { - "tableName": "Shape", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "path", - "columnName": "path", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "Stop", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT NOT NULL, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lat", - "columnName": "lat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "lng", - "columnName": "lng", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "parent", - "columnName": "parent", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "hasWheelChairBoarding", - "columnName": "hasWheelChairBoarding", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "level", - "columnName": "level", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformCode", - "columnName": "platformCode", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Stop_parent", - "unique": false, - "columnNames": [ - "parent" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `${TABLE_NAME}` (`parent`)" - } - ] - }, - { - "tableName": "StopTime", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tripId` TEXT NOT NULL, `stopId` TEXT NOT NULL, `arrivalTime` INTEGER NOT NULL, `departureTime` INTEGER NOT NULL, `headsign` TEXT, `pickupType` INTEGER NOT NULL, `dropOffType` INTEGER NOT NULL, PRIMARY KEY(`tripId`, `stopId`), FOREIGN KEY(`tripId`) REFERENCES `Trip`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`stopId`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tripId", - "columnName": "tripId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "stopId", - "columnName": "stopId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "arrivalTime", - "columnName": "arrivalTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "departureTime", - "columnName": "departureTime", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "headsign", - "columnName": "headsign", - "affinity": "TEXT" - }, - { - "fieldPath": "pickupType", - "columnName": "pickupType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dropOffType", - "columnName": "dropOffType", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tripId", - "stopId" - ] - }, - "indices": [ - { - "name": "index_StopTime_tripId", - "unique": false, - "columnNames": [ - "tripId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_tripId` ON `${TABLE_NAME}` (`tripId`)" - }, - { - "name": "index_StopTime_stopId", - "unique": false, - "columnNames": [ - "stopId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_StopTime_stopId` ON `${TABLE_NAME}` (`stopId`)" - } - ], - "foreignKeys": [ - { - "table": "Trip", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "tripId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Stop", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "stopId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Trip", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `routeId` TEXT NOT NULL, `serviceId` TEXT NOT NULL, `shapeId` TEXT, `tripHeadsign` TEXT NOT NULL, `directionId` TEXT NOT NULL, `blockId` TEXT NOT NULL, `wheelchairAccessible` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`routeId`) REFERENCES `Route`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serviceId`) REFERENCES `Service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`shapeId`) REFERENCES `Shape`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "routeId", - "columnName": "routeId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "serviceId", - "columnName": "serviceId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shapeId", - "columnName": "shapeId", - "affinity": "TEXT" - }, - { - "fieldPath": "tripHeadsign", - "columnName": "tripHeadsign", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "directionId", - "columnName": "directionId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "blockId", - "columnName": "blockId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "wheelchairAccessible", - "columnName": "wheelchairAccessible", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_Trip_shapeId", - "unique": false, - "columnNames": [ - "shapeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_shapeId` ON `${TABLE_NAME}` (`shapeId`)" - }, - { - "name": "index_Trip_routeId", - "unique": false, - "columnNames": [ - "routeId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_routeId` ON `${TABLE_NAME}` (`routeId`)" - } - ], - "foreignKeys": [ - { - "table": "Route", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "routeId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Service", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "serviceId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Shape", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "shapeId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "VersionMetadata", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`type`))", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type" - ] - } - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e0f07bf1af88b2e37b5ad7c38a3fb2a')" - ] - } -} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/moe/lava/banksia/di/PlatformModule.android.kt b/shared/src/androidMain/kotlin/moe/lava/banksia/di/PlatformModule.android.kt deleted file mode 100644 index 0447f4b..0000000 --- a/shared/src/androidMain/kotlin/moe/lava/banksia/di/PlatformModule.android.kt +++ /dev/null @@ -1,25 +0,0 @@ -package moe.lava.banksia.di - -import android.content.Context -import androidx.room.Room -import androidx.room.RoomDatabase -import moe.lava.banksia.room.Database -import org.koin.core.parameter.ParametersHolder -import org.koin.core.scope.Scope -import org.koin.dsl.module - -class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder { - override fun getBuilder(): RoomDatabase.Builder { - val appContext = ctx.applicationContext - val dbFile = appContext.getDatabasePath("room.db") - return Room.databaseBuilder( - context = appContext, - name = dbFile.absolutePath - ) - } -} - -actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder = - AndroidDatabaseBuilder(get()) - -internal actual val ExtPlatformModule = module { } diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt deleted file mode 100644 index 8658342..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt +++ /dev/null @@ -1,17 +0,0 @@ -package moe.lava.banksia.di - -import moe.lava.banksia.room.Database -import org.koin.dsl.module - -val CommonModules = module { - includes(PlatformModule) - - single { Database.build(get().getBuilder()) } - single { get().versionMetadataDao } - single { get().routeDao } - single { get().serviceDao } - single { get().shapeDao } - single { get().stopDao } - single { get().stopTimeDao } - single { get().tripDao } -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/di/PlatformModule.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/di/PlatformModule.kt deleted file mode 100644 index 6f29f14..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/di/PlatformModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package moe.lava.banksia.di - -import androidx.room.RoomDatabase -import moe.lava.banksia.room.Database -import org.koin.core.module.Module -import org.koin.core.parameter.ParametersHolder -import org.koin.core.scope.Scope -import org.koin.dsl.module - -interface PlatformDatabaseBuilder { - fun getBuilder(): RoomDatabase.Builder -} - -expect fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder - -internal expect val ExtPlatformModule: Module - -internal val PlatformModule = module { - includes(ExtPlatformModule) - single { provideDatabaseBuilder(it) } -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTime.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTime.kt deleted file mode 100644 index 682839d..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTime.kt +++ /dev/null @@ -1,14 +0,0 @@ -package moe.lava.banksia.model - -import kotlinx.serialization.Serializable - -@Serializable -data class StopTime( - val tripId: String, - val stopId: String, - val arrivalTime: FutureTime, - val departureTime: FutureTime, - val headsign: String?, - val pickupType: Int, - val dropOffType: Int, -) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTimeDated.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTimeDated.kt deleted file mode 100644 index 55288fa..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTimeDated.kt +++ /dev/null @@ -1,26 +0,0 @@ -package moe.lava.banksia.model - -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalDateTime -import kotlinx.serialization.Serializable - -@Serializable -data class StopTimeDated( - val tripId: String, - val stopId: String, - val arrivalTime: LocalDateTime, - val departureTime: LocalDateTime, - val headsign: String?, - val pickupType: Int, - val dropOffType: Int, -) - -fun StopTime.atDate(date: LocalDate) = StopTimeDated( - tripId = tripId, - stopId = stopId, - arrivalTime = arrivalTime.atDate(date), - departureTime = departureTime.atDate(date), - headsign = headsign, - pickupType = pickupType, - dropOffType = dropOffType, -) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt deleted file mode 100644 index 81d3f8d..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt +++ /dev/null @@ -1,15 +0,0 @@ -package moe.lava.banksia.model - -import kotlinx.serialization.Serializable - -@Serializable -data class Trip( - val id: String, - val routeId: String, - val service: Service, - val shapeId: String?, - val tripHeadsign: String, - val directionId: String, - val blockId: String, - val wheelchairAccessible: String, -) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt deleted file mode 100644 index 89bc24a..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt +++ /dev/null @@ -1,60 +0,0 @@ -package moe.lava.banksia.room - -import androidx.room.AutoMigration -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import androidx.sqlite.driver.bundled.BundledSQLiteDriver -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO -import moe.lava.banksia.room.converter.RouteTypeConverter -import moe.lava.banksia.room.dao.RouteDao -import moe.lava.banksia.room.dao.ServiceDao -import moe.lava.banksia.room.dao.ShapeDao -import moe.lava.banksia.room.dao.StopDao -import moe.lava.banksia.room.dao.StopTimeDao -import moe.lava.banksia.room.dao.TripDao -import moe.lava.banksia.room.dao.VersionMetadataDao -import moe.lava.banksia.room.entity.RouteEntity -import moe.lava.banksia.room.entity.ServiceEntity -import moe.lava.banksia.room.entity.ShapeEntity -import moe.lava.banksia.room.entity.StopEntity -import moe.lava.banksia.room.entity.StopTimeEntity -import moe.lava.banksia.room.entity.TripEntity -import moe.lava.banksia.room.entity.VersionMetadataEntity -import androidx.room.Database as DatabaseAnnotation - -@DatabaseAnnotation( - version = 9, - entities = [ - RouteEntity::class, - ServiceEntity::class, - ShapeEntity::class, - StopEntity::class, - StopTimeEntity::class, - TripEntity::class, - VersionMetadataEntity::class, - ], - autoMigrations = [ - AutoMigration(from = 1, to = 2), - AutoMigration(from = 2, to = 3), - ] -) -@TypeConverters(RouteTypeConverter::class) -abstract class Database : RoomDatabase() { - abstract val versionMetadataDao: VersionMetadataDao - abstract val routeDao: RouteDao - abstract val serviceDao: ServiceDao - abstract val shapeDao: ShapeDao - abstract val stopDao: StopDao - abstract val stopTimeDao: StopTimeDao - abstract val tripDao: TripDao - - companion object { - fun build(base: Builder) = - base.fallbackToDestructiveMigration(true) - .setDriver(BundledSQLiteDriver()) - .setQueryCoroutineContext(Dispatchers.IO) -// .fallbackToDestructiveMigration(true) - .build() - } -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/RouteTypeConverter.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/RouteTypeConverter.kt deleted file mode 100644 index 8927f14..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/RouteTypeConverter.kt +++ /dev/null @@ -1,12 +0,0 @@ -package moe.lava.banksia.room.converter - -import androidx.room.TypeConverter -import moe.lava.banksia.model.RouteType - -object RouteTypeConverter { - @TypeConverter - fun from(value: Int) = RouteType.entries.first { it.value == value } - - @TypeConverter - fun to(routeType: RouteType) = routeType.value -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/ShapePathConverter.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/ShapePathConverter.kt deleted file mode 100644 index 08a8064..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/ShapePathConverter.kt +++ /dev/null @@ -1,43 +0,0 @@ -package moe.lava.banksia.room.converter - -import androidx.room.TypeConverter -import moe.lava.banksia.model.ShapePath -import moe.lava.banksia.util.Point - -object ShapePathConverter { - @TypeConverter - fun from(value: ByteArray): ShapePath { - return value - .asIterable() - .chunked(8) { - (it[0].toLong() and 0xFF) or - (it[1].toLong() and 0xFF shl 8) or - (it[2].toLong() and 0xFF shl 16) or - (it[3].toLong() and 0xFF shl 24) or - (it[4].toLong() and 0xFF shl 32) or - (it[5].toLong() and 0xFF shl 40) or - (it[6].toLong() and 0xFF shl 48) or - (it[7].toLong() and 0xFF shl 56) - } - .map { Double.fromBits(it) } - .chunked(2) - .map { (lat, lng) -> Point(lat, lng) } - } - - @TypeConverter - fun to(path: ShapePath): ByteArray { - return path - .flatMap { (lat, lng) -> listOf(lat.toBits(), lng.toBits()) } - .flatMap { i -> listOf( - i.toByte(), - (i shr 8).toByte(), - (i shr 16).toByte(), - (i shr 24).toByte(), - (i shr 32).toByte(), - (i shr 40).toByte(), - (i shr 48).toByte(), - (i shr 56).toByte(), - ) } - .toByteArray() - } -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt deleted file mode 100644 index 0174f0f..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt +++ /dev/null @@ -1,49 +0,0 @@ -package moe.lava.banksia.room.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy.Companion.REPLACE -import androidx.room.Query -import moe.lava.banksia.room.entity.RouteEntity -import moe.lava.banksia.room.entity.StopEntity - -@Dao -interface RouteDao { - @Query("SELECT * FROM Route") - suspend fun getAll(): List - - @Query("SELECT * FROM Route WHERE id == :id") - suspend fun get(id: String): RouteEntity? - - @Insert - suspend fun insertAll(vararg routes: RouteEntity) - - @Insert(onConflict = REPLACE) - suspend fun insertOrReplaceAll(vararg routes: RouteEntity) - - @Delete - suspend fun delete(route: RouteEntity) - - @Query("DELETE FROM Route") - suspend fun deleteAll() - - @Query(""" - SELECT Stop.* FROM Stop - INNER JOIN StopTime ON StopTime.stopId == Stop.id - INNER JOIN Trip ON Trip.id == StopTime.tripId - WHERE Trip.routeId == :id - GROUP BY Stop.id - """) - suspend fun stops(id: String): List - - @Query(""" - SELECT Stop.* FROM Stop - INNER JOIN Stop Child ON Child.parent == Stop.id - INNER JOIN StopTime ON StopTime.stopId == Child.id - INNER JOIN Trip ON Trip.id == StopTime.tripId - WHERE Trip.routeId == :id - GROUP BY Stop.id - """) - suspend fun stopsParent(id: String): List -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ServiceDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ServiceDao.kt deleted file mode 100644 index 6fc2906..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ServiceDao.kt +++ /dev/null @@ -1,29 +0,0 @@ -package moe.lava.banksia.room.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy.Companion.REPLACE -import androidx.room.Query -import moe.lava.banksia.room.entity.ServiceEntity - -@Dao -interface ServiceDao { - @Query("SELECT * FROM Service") - suspend fun getAll(): List - - @Query("SELECT * FROM Service WHERE id == :id") - suspend fun get(id: String): ServiceEntity? - - @Insert - suspend fun insertAll(vararg services: ServiceEntity) - - @Insert(onConflict = REPLACE) - suspend fun insertOrReplaceAll(vararg services: ServiceEntity) - - @Delete - suspend fun delete(service: ServiceEntity) - - @Query("DELETE FROM Service") - suspend fun deleteAll() -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt deleted file mode 100644 index c48735a..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt +++ /dev/null @@ -1,22 +0,0 @@ -package moe.lava.banksia.room.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.Query -import moe.lava.banksia.room.entity.ShapeEntity - -@Dao -interface ShapeDao { - @Query("SELECT * FROM Shape WHERE id == :id") - suspend fun get(id: String): ShapeEntity? - - @Insert - suspend fun insertAll(vararg shapes: ShapeEntity) - - @Delete - suspend fun delete(shape: ShapeEntity) - - @Query("DELETE FROM Shape") - suspend fun deleteAll() -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopDao.kt deleted file mode 100644 index f6b2ef2..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopDao.kt +++ /dev/null @@ -1,32 +0,0 @@ -package moe.lava.banksia.room.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy.Companion.REPLACE -import androidx.room.Query -import moe.lava.banksia.room.entity.StopEntity - -@Dao -interface StopDao { - @Query("SELECT * FROM Stop") - suspend fun getAll(): List - - @Query("SELECT * FROM Stop WHERE id == :id") - suspend fun get(id: String): StopEntity? - - @Query("SELECT * FROM Stop WHERE id IN (:ids)") - suspend fun get(ids: List): List - - @Insert - suspend fun insertAll(vararg stops: StopEntity) - - @Insert(onConflict = REPLACE) - suspend fun insertOrReplaceAll(vararg stops: StopEntity) - - @Delete - suspend fun delete(stop: StopEntity) - - @Query("DELETE FROM Stop") - suspend fun deleteAll() -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt deleted file mode 100644 index d5e1744..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt +++ /dev/null @@ -1,44 +0,0 @@ -package moe.lava.banksia.room.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy.Companion.REPLACE -import androidx.room.Query -import moe.lava.banksia.room.entity.StopTimeEntity - -@Dao -interface StopTimeDao { - @Query("SELECT * FROM StopTime") - suspend fun getAll(): List - - @Query("SELECT * FROM StopTime WHERE tripId == :tripId") - suspend fun getForTrip(tripId: String): StopTimeEntity? - - @Query("SELECT * FROM StopTime WHERE tripId IN (:tripIds)") - suspend fun getForTrips(tripIds: List): List - - @Query("SELECT * FROM StopTime WHERE stopId == :stopId") - suspend fun getForStop(stopId: String): List - - @Query(""" - SELECT * FROM StopTime - INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end` - INNER JOIN Trip ON Trip.serviceId == Service.id - WHERE StopTime.tripId == Trip.id - AND StopTime.stopId == :stopId - """) - suspend fun getForStopDated(stopId: String, days: Int, date: Int): List - - @Insert - suspend fun insertAll(vararg stopTimes: StopTimeEntity) - - @Insert(onConflict = REPLACE) - suspend fun insertOrReplaceAll(vararg stopTimes: StopTimeEntity) - - @Delete - suspend fun delete(stopTime: StopTimeEntity) - - @Query("DELETE FROM StopTime") - suspend fun deleteAll() -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/TripDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/TripDao.kt deleted file mode 100644 index 9778a1a..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/TripDao.kt +++ /dev/null @@ -1,32 +0,0 @@ -package moe.lava.banksia.room.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy.Companion.REPLACE -import androidx.room.Query -import moe.lava.banksia.room.entity.TripEntity - -@Dao -interface TripDao { - @Query("SELECT * FROM Trip") - suspend fun getAll(): List - - @Query("SELECT * FROM Trip WHERE id == :id") - suspend fun get(id: String): TripEntity? - - @Query("SELECT * FROM Trip WHERE routeId == :id") - suspend fun getByRoute(id: String): List - - @Insert - suspend fun insertAll(vararg trips: TripEntity) - - @Insert(onConflict = REPLACE) - suspend fun insertOrReplaceAll(vararg trips: TripEntity) - - @Delete - suspend fun delete(trip: TripEntity) - - @Query("DELETE FROM Trip") - suspend fun deleteAll() -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/VersionMetadataDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/VersionMetadataDao.kt deleted file mode 100644 index b96102e..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/VersionMetadataDao.kt +++ /dev/null @@ -1,27 +0,0 @@ -package moe.lava.banksia.room.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy.Companion.REPLACE -import androidx.room.Query -import moe.lava.banksia.room.entity.VersionMetadataEntity - -@Dao -interface VersionMetadataDao { - @Query("SELECT * FROM VersionMetadata WHERE type == :type") - suspend fun get(type: String): VersionMetadataEntity? - - @Query("SELECT * FROM VersionMetadata") - suspend fun getAll(): List - - @Insert(onConflict = REPLACE) - suspend fun update(vararg data: VersionMetadataEntity) - - suspend fun update(vararg data: Pair) { - update(*data.map { (type, lastUpdated) -> VersionMetadataEntity(type, lastUpdated) }.toTypedArray()) - } - - suspend fun update(lastUpdated: Long, types: Collection) { - update(*types.map { VersionMetadataEntity(it, lastUpdated) }.toTypedArray()) - } -} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/RouteEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/RouteEntity.kt deleted file mode 100644 index cc690d6..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/RouteEntity.kt +++ /dev/null @@ -1,18 +0,0 @@ -package moe.lava.banksia.room.entity - -import androidx.room.Entity -import androidx.room.PrimaryKey -import moe.lava.banksia.model.Route -import moe.lava.banksia.model.RouteType - -@Entity("Route") -data class RouteEntity( - @PrimaryKey val id: String, - val type: RouteType, - val number: String?, - val name: String, -) { - fun asModel() = Route(id, type, number, name) -} - -fun Route.asEntity() = RouteEntity(id, type, number, name) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt deleted file mode 100644 index 027aaa8..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt +++ /dev/null @@ -1,31 +0,0 @@ -package moe.lava.banksia.room.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import kotlinx.datetime.LocalDate -import moe.lava.banksia.model.Service -import moe.lava.banksia.util.deserialiseDaysBitflag -import moe.lava.banksia.util.serialise - -@Entity("Service") -data class ServiceEntity( - @PrimaryKey val id: String, - @ColumnInfo(index = true) val days: Int, - val start: Int, - val end: Int, -) { - fun asModel() = Service( - id, - days.deserialiseDaysBitflag(), - LocalDate.fromEpochDays(start), - LocalDate.fromEpochDays(end), - ) -} - -fun Service.asEntity() = ServiceEntity( - id, - days.serialise(), - start.toEpochDays().toInt(), - end.toEpochDays().toInt(), -) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ShapeEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ShapeEntity.kt deleted file mode 100644 index 87ca671..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ShapeEntity.kt +++ /dev/null @@ -1,19 +0,0 @@ -package moe.lava.banksia.room.entity - -import androidx.room.Entity -import androidx.room.PrimaryKey -import androidx.room.TypeConverters -import moe.lava.banksia.model.Shape -import moe.lava.banksia.model.ShapePath -import moe.lava.banksia.room.converter.ShapePathConverter - -@Entity("Shape") -@TypeConverters(ShapePathConverter::class) -data class ShapeEntity( - @PrimaryKey val id: String, - val path: ShapePath, -) { - fun asModel() = Shape(id, path) -} - -fun Shape.asEntity() = ShapeEntity(id, path) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopEntity.kt deleted file mode 100644 index 9c6cf15..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopEntity.kt +++ /dev/null @@ -1,23 +0,0 @@ -package moe.lava.banksia.room.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import moe.lava.banksia.model.Stop -import moe.lava.banksia.util.Point - -@Entity("Stop") -data class StopEntity( - @PrimaryKey val id: String, - val name: String, - val lat: Double, - val lng: Double, - @ColumnInfo(index = true) val parent: String, - val hasWheelChairBoarding: Boolean, - val level: String, - val platformCode: String, -) { - fun asModel() = Stop(id, name, Point(lat, lng), parent, hasWheelChairBoarding, level, platformCode) -} - -fun Stop.asEntity() = StopEntity(id, name, pos.lat, pos.lng, parent, hasWheelChairBoarding, level, platformCode) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopTimeEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopTimeEntity.kt deleted file mode 100644 index bb20ff1..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopTimeEntity.kt +++ /dev/null @@ -1,53 +0,0 @@ -package moe.lava.banksia.room.entity - -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.Index -import kotlinx.serialization.ExperimentalSerializationApi -import moe.lava.banksia.model.FutureTime -import moe.lava.banksia.model.FutureTime.Companion.asInt -import moe.lava.banksia.model.StopTime - -@Entity( - "StopTime", - primaryKeys = ["tripId", "stopId"], - indices = [ - Index("tripId", unique = false), - Index("stopId", unique = false), - ], - foreignKeys = [ - ForeignKey(TripEntity::class, parentColumns = ["id"], childColumns = ["tripId"], onDelete = CASCADE), - ForeignKey(StopEntity::class, parentColumns = ["id"], childColumns = ["stopId"], onDelete = CASCADE), - ] -) -data class StopTimeEntity( - val tripId: String, - val stopId: String, - val arrivalTime: Int, - val departureTime: Int, - val headsign: String?, - val pickupType: Int, - val dropOffType: Int, -) { - fun asModel() = StopTime( - tripId, - stopId, - FutureTime.fromInt(arrivalTime), - FutureTime.fromInt(departureTime), - headsign, - pickupType, - dropOffType, - ) -} - -@OptIn(ExperimentalSerializationApi::class) -fun StopTime.asEntity() = StopTimeEntity( - tripId, - stopId, - arrivalTime.asInt(), - departureTime.asInt(), - headsign, - pickupType, - dropOffType, -) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt deleted file mode 100644 index 3753d44..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt +++ /dev/null @@ -1,49 +0,0 @@ -package moe.lava.banksia.room.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.Index -import androidx.room.PrimaryKey -import moe.lava.banksia.model.Trip - -@Entity( - "Trip", - foreignKeys = [ - ForeignKey(RouteEntity::class, parentColumns = ["id"], childColumns = ["routeId"], onDelete = CASCADE), - ForeignKey(ServiceEntity::class, parentColumns = ["id"], childColumns = ["serviceId"], onDelete = CASCADE), - ForeignKey(ShapeEntity::class, parentColumns = ["id"], childColumns = ["shapeId"], onDelete = CASCADE), - ], - indices = [Index("shapeId")], -) -data class TripEntity( - @PrimaryKey val id: String, - @ColumnInfo(index = true) val routeId: String, - val serviceId: String, - val shapeId: String?, - val tripHeadsign: String, - val directionId: String, - val blockId: String, - val wheelchairAccessible: String, -) - -fun Trip.Companion.from(tripEntity: TripEntity, serviceEntity: ServiceEntity): Trip { - if (tripEntity.serviceId != serviceEntity.id) { - throw IllegalArgumentException("trip and service id mismatch (${tripEntity.serviceId} != ${serviceEntity.id})") - } - return with(tripEntity) { - Trip( - id = id, - routeId = routeId, - service = serviceEntity.asModel(), - shapeId = shapeId, - tripHeadsign = tripHeadsign, - directionId = directionId, - blockId = blockId, - wheelchairAccessible = wheelchairAccessible - ) - } -} - -fun Trip.asEntity() = TripEntity(id, routeId, service.id, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/VersionMetadataEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/VersionMetadataEntity.kt deleted file mode 100644 index fc00b44..0000000 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/VersionMetadataEntity.kt +++ /dev/null @@ -1,19 +0,0 @@ -package moe.lava.banksia.room.entity - -import androidx.room.Entity -import androidx.room.PrimaryKey -import moe.lava.banksia.model.VersionMetadata - -@Entity( - "VersionMetadata", -) -data class VersionMetadataEntity( - /** Entity type this metadata applies to */ - @PrimaryKey val type: String, - /** Last updated */ - val lastUpdated: Long, -) { - fun asModel() = VersionMetadata(type, lastUpdated) -} - -fun VersionMetadata.asEntity() = VersionMetadataEntity(type, lastUpdated) diff --git a/shared/src/iosMain/kotlin/moe/lava/banksia/di/PlatformModule.ios.kt b/shared/src/iosMain/kotlin/moe/lava/banksia/di/PlatformModule.ios.kt deleted file mode 100644 index 8597856..0000000 --- a/shared/src/iosMain/kotlin/moe/lava/banksia/di/PlatformModule.ios.kt +++ /dev/null @@ -1,18 +0,0 @@ -package moe.lava.banksia.di - -import androidx.room.RoomDatabase -import moe.lava.banksia.room.Database -import org.koin.core.parameter.ParametersHolder -import org.koin.core.scope.Scope -import org.koin.dsl.module - -class IosDatabaseBuilder() : PlatformDatabaseBuilder { - override fun getBuilder(): RoomDatabase.Builder { - TODO("Not yet implemented") - } -} - -actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder = - IosDatabaseBuilder() - -internal actual val ExtPlatformModule = module { } diff --git a/shared/src/jvmMain/kotlin/moe/lava/banksia/di/PlatformModule.jvm.kt b/shared/src/jvmMain/kotlin/moe/lava/banksia/di/PlatformModule.jvm.kt deleted file mode 100644 index 3e93241..0000000 --- a/shared/src/jvmMain/kotlin/moe/lava/banksia/di/PlatformModule.jvm.kt +++ /dev/null @@ -1,23 +0,0 @@ -package moe.lava.banksia.di - -import androidx.room.Room -import androidx.room.RoomDatabase -import moe.lava.banksia.room.Database -import org.koin.core.parameter.ParametersHolder -import org.koin.core.scope.Scope -import org.koin.dsl.module -import java.io.File - -class JvmDatabaseBuilder() : PlatformDatabaseBuilder { - override fun getBuilder(): RoomDatabase.Builder { - val dbFile = File("./data/room.db") - return Room.databaseBuilder( - name = dbFile.absolutePath, - ) - } -} - -actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder = - JvmDatabaseBuilder() - -internal actual val ExtPlatformModule = module { } diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index 201a10f..b599bc6 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -41,7 +41,9 @@ kotlin { sourceSets { androidMain.dependencies { + implementation(libs.compose.ui.tooling.preview) implementation(libs.play.services.location) + implementation(projects.ui.shared) } commonMain.dependencies { implementation(libs.compose.components.resources) @@ -67,8 +69,9 @@ kotlin { implementation(libs.moko.geo.compose) implementation(libs.ui.backhandler) - implementation(projects.client) - implementation(projects.shared) + implementation(projects.core) + implementation(projects.core.data) + implementation(projects.core.stoptime) implementation(projects.ui.maps) implementation(projects.ui.shared) } diff --git a/ui/maps/build.gradle.kts b/ui/maps/build.gradle.kts index 324b0b3..4e859d1 100644 --- a/ui/maps/build.gradle.kts +++ b/ui/maps/build.gradle.kts @@ -49,7 +49,7 @@ kotlin { implementation(libs.compose.material3) implementation(libs.compose.ui) - implementation(projects.shared) + implementation(projects.core) implementation(projects.ui.shared) } } diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapLibreMaps.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapLibreMaps.kt index 1df9cb1..d76c1f4 100644 --- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapLibreMaps.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapLibreMaps.kt @@ -6,12 +6,15 @@ import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.safeDrawing import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch import kotlinx.serialization.json.JsonObject -import moe.lava.banksia.Constants +import moe.lava.banksia.core.Constants import moe.lava.banksia.ui.map.mappers.routeColorExpression +import moe.lava.banksia.ui.map.mappers.toMapPosition import moe.lava.banksia.ui.platform.BanksiaTheme import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.rememberCameraState @@ -26,6 +29,7 @@ import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.util.ClickResult import org.maplibre.spatialk.geojson.Feature import org.maplibre.spatialk.geojson.Geometry +import kotlin.time.Duration.Companion.seconds @Composable internal fun MapLibreMaps( @@ -34,7 +38,7 @@ internal fun MapLibreMaps( positionState: MapsPositionState, stops: GeoJsonData.Features?, // vehicles: GeoJsonData.Features?, - stopInnerColor: Color, + stopInnerColor: Color = BanksiaTheme.colors.surface, onStopClicked: (Feature) -> Unit, ) { val camPos = rememberCameraState( @@ -43,6 +47,17 @@ internal fun MapLibreMaps( target = MELBOURNE_POS ) ) + val scope = rememberCoroutineScope() + scope.launch { + positionState.updates.collect { + val (position, box) = it.toMapPosition() + if (box != null) { + camPos.animateTo(box, duration = 1.seconds) + } else { + camPos.animateTo(position, duration = 1.seconds) + } + } + } val variant = if (isSystemInDarkTheme()) "dark" else "light" @@ -63,7 +78,7 @@ internal fun MapLibreMaps( CircleLayer( id = "maps-stops0", source = stopsSource, - color = const(BanksiaTheme.colors.surface), + color = const(stopInnerColor), radius = const(3.dp), strokeWidth = const(2.dp), strokeColor = routeColorExpression, diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/Maps.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/Maps.kt index 52b7250..92a9695 100644 --- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/Maps.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/Maps.kt @@ -4,11 +4,11 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import moe.lava.banksia.core.util.Point import moe.lava.banksia.ui.map.mappers.asFeatures import moe.lava.banksia.ui.map.mappers.toPosition import moe.lava.banksia.ui.map.util.Marker import moe.lava.banksia.ui.platform.BanksiaTheme -import moe.lava.banksia.util.Point internal val MELBOURNE = Point(-37.8136, 144.9631) internal val MELBOURNE_POS = MELBOURNE.toPosition() diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapsPositionState.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapsPositionState.kt index a9fe8b2..94421a7 100644 --- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapsPositionState.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapsPositionState.kt @@ -7,16 +7,18 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch -import moe.lava.banksia.util.Point +import moe.lava.banksia.ui.map.util.CameraPosition class MapsPositionState internal constructor( private val scope: CoroutineScope ) { - internal val updates: SharedFlow + internal val updates: SharedFlow field = MutableSharedFlow() - fun update(position: Point) { - scope.launch { updates.emit(position) } + fun update(position: CameraPosition) { + scope.launch { + updates.emit(position) + } } } diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/CameraPosition.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/CameraPosition.kt new file mode 100644 index 0000000..b463c18 --- /dev/null +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/CameraPosition.kt @@ -0,0 +1,15 @@ +package moe.lava.banksia.ui.map.mappers + +import moe.lava.banksia.ui.map.util.CameraPosition +import org.maplibre.spatialk.geojson.BoundingBox +import org.maplibre.compose.camera.CameraPosition as MLCameraPosition + +internal fun CameraPosition.toMapPosition() = Pair( + MLCameraPosition(target = this.centre.toPosition(), zoom = 16.0), + this.bounds?.let { + BoundingBox( + southwest = it.southwest.toPosition(), + northeast = it.northeast.toPosition(), + ) + } +) diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Marker.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Marker.kt index 32a910c..3fe99c2 100644 --- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Marker.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Marker.kt @@ -1,7 +1,7 @@ package moe.lava.banksia.ui.map.mappers import kotlinx.serialization.Serializable -import moe.lava.banksia.model.RouteType +import moe.lava.banksia.core.model.RouteType import moe.lava.banksia.ui.map.util.Marker import org.maplibre.compose.sources.GeoJsonData import org.maplibre.spatialk.geojson.FeatureCollection diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Position.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Position.kt index c137394..ed568c2 100644 --- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Position.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Position.kt @@ -1,6 +1,6 @@ package moe.lava.banksia.ui.map.mappers -import moe.lava.banksia.util.Point +import moe.lava.banksia.core.util.Point import org.maplibre.spatialk.geojson.Position internal fun Point.toPosition() = Position(lng, lat) diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/RouteType.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/RouteType.kt index 523e438..584c76f 100644 --- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/RouteType.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/RouteType.kt @@ -1,7 +1,7 @@ package moe.lava.banksia.ui.map.mappers import androidx.compose.runtime.Composable -import moe.lava.banksia.model.RouteType +import moe.lava.banksia.core.model.RouteType import moe.lava.banksia.ui.extensions.getUIProperties import moe.lava.banksia.ui.platform.BanksiaTheme import org.maplibre.compose.expressions.dsl.case diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt index 710cebb..aba2858 100644 --- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt @@ -1,6 +1,6 @@ package moe.lava.banksia.ui.map.util -import moe.lava.banksia.util.Point +import moe.lava.banksia.core.util.Point data class CameraPosition( val centre: Point = Point(-37.8136, 144.9631), diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt index 4adf3b1..9381262 100644 --- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt @@ -1,5 +1,5 @@ package moe.lava.banksia.ui.map.util -import moe.lava.banksia.util.Point +import moe.lava.banksia.core.util.Point data class CameraPositionBounds(val northeast: Point, val southwest: Point) diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Marker.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Marker.kt index 9326b2a..ac33868 100644 --- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Marker.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Marker.kt @@ -1,8 +1,8 @@ package moe.lava.banksia.ui.map.util import kotlinx.serialization.Serializable -import moe.lava.banksia.model.RouteType -import moe.lava.banksia.util.Point +import moe.lava.banksia.core.model.RouteType +import moe.lava.banksia.core.util.Point @Serializable sealed class Marker { diff --git a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt index 146d74b..04b8dc6 100644 --- a/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt @@ -1,6 +1,6 @@ package moe.lava.banksia.ui.map.util import androidx.compose.ui.graphics.Color -import moe.lava.banksia.util.Point +import moe.lava.banksia.core.util.Point data class Polyline(val points: List, val colour: Color) diff --git a/ui/shared/build.gradle.kts b/ui/shared/build.gradle.kts index c784fed..2a78572 100644 --- a/ui/shared/build.gradle.kts +++ b/ui/shared/build.gradle.kts @@ -16,6 +16,10 @@ kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } + + androidResources { + enable = true + } } compilerOptions { @@ -35,7 +39,7 @@ kotlin { implementation(libs.compose.ui) implementation(libs.compose.ui.tooling.preview) - implementation(projects.shared) + implementation(projects.core) } } } @@ -47,4 +51,5 @@ dependencies { compose.resources { publicResClass = true packageOfResClass = "moe.lava.banksia.resources" + generateResClass = always } diff --git a/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_down.xml b/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_down.xml new file mode 100644 index 0000000..ac49572 --- /dev/null +++ b/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_down.xml @@ -0,0 +1,9 @@ + + + diff --git a/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_up.xml b/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_up.xml new file mode 100644 index 0000000..322fa56 --- /dev/null +++ b/ui/shared/src/commonMain/composeResources/drawable/arrow_drop_up.xml @@ -0,0 +1,9 @@ + + + diff --git a/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt index e84d765..90914ae 100644 --- a/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt +++ b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt @@ -11,10 +11,10 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import moe.lava.banksia.model.RouteType -import moe.lava.banksia.model.RouteType.MetroBus -import moe.lava.banksia.model.RouteType.MetroTrain -import moe.lava.banksia.model.RouteType.MetroTram +import moe.lava.banksia.core.model.RouteType +import moe.lava.banksia.core.model.RouteType.MetroBus +import moe.lava.banksia.core.model.RouteType.MetroTrain +import moe.lava.banksia.core.model.RouteType.MetroTram import moe.lava.banksia.ui.extensions.getUIProperties import org.jetbrains.compose.resources.painterResource diff --git a/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt index 992b910..805f572 100644 --- a/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt +++ b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt @@ -1,8 +1,8 @@ package moe.lava.banksia.ui.extensions import androidx.compose.ui.graphics.Color +import moe.lava.banksia.core.model.RouteType import moe.lava.banksia.data.ptv.structures.PtvRouteType -import moe.lava.banksia.model.RouteType import moe.lava.banksia.resources.Res import moe.lava.banksia.resources.bus import moe.lava.banksia.resources.bus_background diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt index 453e1ee..f74dc1a 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/App.kt @@ -3,7 +3,6 @@ package moe.lava.banksia.ui import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi -import moe.lava.banksia.di.CommonModules import moe.lava.banksia.ui.di.AppModule import moe.lava.banksia.ui.screens.map.MapScreen import org.koin.compose.KoinMultiplatformApplication @@ -14,7 +13,7 @@ import org.koin.dsl.koinConfiguration @Composable fun App() { KoinMultiplatformApplication(config = koinConfiguration { - modules(CommonModules, AppModule) + modules(AppModule) }) { MapScreen() } diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt index 4c93644..cff36fb 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt @@ -1,12 +1,13 @@ package moe.lava.banksia.ui.di -import moe.lava.banksia.client.di.ClientModule +import moe.lava.banksia.core.data.dataDiModule import moe.lava.banksia.ui.screens.map.MapScreenViewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val AppModule = module { - includes(ClientModule) + includes(dataDiModule) + // ViewModel viewModelOf(::MapScreenViewModel) } diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/InfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/InfoPanel.kt index fa0354d..9fb37d7 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/InfoPanel.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/InfoPanel.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeContent import androidx.compose.foundation.layout.size @@ -27,18 +28,26 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.coerceAtMost import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay -import moe.lava.banksia.ui.screens.map.MapScreenEvent -import moe.lava.banksia.ui.state.InfoPanelState import kotlin.time.Duration.Companion.milliseconds +sealed class InfoPanelEvent + +sealed class InfoPanelState { + abstract val loading: Boolean + + data object None : InfoPanelState() { + override val loading = false + } +} + @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun InfoPanel( + modifier: Modifier = Modifier, state: InfoPanelState, - onEvent: (MapScreenEvent) -> Unit, + onEvent: (InfoPanelEvent) -> Unit, onPeekHeightChange: (Dp) -> Unit, ) { if (state is InfoPanelState.None) @@ -57,18 +66,20 @@ fun InfoPanel( } Column( - Modifier + modifier = modifier .fillMaxWidth() .padding(horizontal = 24.dp) + .heightIn(min = 350.dp) .onSizeChanged { - onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) }) +// onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) }) + onPeekHeightChange(350.dp) } ) { Box { when (state) { - is InfoPanelState.Route -> RouteInfoPanel(state, onEvent) - is InfoPanelState.Stop -> StopInfoPanel(state, onEvent) - is InfoPanelState.Trip -> TripInfoPanel(state, onEvent) + is RouteInfoPanelState -> RouteInfoPanel(state, onEvent) + is StopInfoPanelState -> StopInfoPanel(state, onEvent) + is TripInfoPanelState -> TripInfoPanel(state, onEvent) is InfoPanelState.None -> throw UnsupportedOperationException() } diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/RouteInfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/RouteInfoPanel.kt index b55b7c1..a1a97d3 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/RouteInfoPanel.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/RouteInfoPanel.kt @@ -9,14 +9,22 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import moe.lava.banksia.core.model.RouteType import moe.lava.banksia.ui.components.RouteIcon -import moe.lava.banksia.ui.screens.map.MapScreenEvent -import moe.lava.banksia.ui.state.InfoPanelState + +sealed class RouteInfoPanelEvent : InfoPanelEvent() + +data class RouteInfoPanelState( + val name: String, + val type: RouteType, +) : InfoPanelState() { + override val loading = false +} @Composable internal fun RouteInfoPanel( - state: InfoPanelState.Route, - onEvent: (MapScreenEvent) -> Unit, + state: RouteInfoPanelState, + onEvent: (RouteInfoPanelEvent) -> Unit, ) { Column(Modifier.fillMaxWidth()) { Row { diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt index 731ef88..369721c 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt @@ -1,63 +1,358 @@ package moe.lava.banksia.ui.layout.info +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedListItem +import androidx.compose.material3.ShapeDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import moe.lava.banksia.ui.screens.map.MapScreenEvent -import moe.lava.banksia.ui.state.InfoPanelState +import moe.lava.banksia.resources.Res +import moe.lava.banksia.resources.arrow_drop_down +import moe.lava.banksia.resources.arrow_drop_up +import moe.lava.banksia.ui.extensions.BUS_ORANGE +import moe.lava.banksia.ui.extensions.TRAIN_BLUE +import moe.lava.banksia.ui.platform.BanksiaTheme +import org.jetbrains.compose.resources.painterResource +import kotlin.time.Clock +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Instant + +sealed class StopInfoPanelEvent : InfoPanelEvent() { + data object ToggleGrouping : StopInfoPanelEvent() +} + +data class StopInfoPanelState( + val id: String, + val name: String, + val subname: String? = null, + val departures: List? = null, +) : InfoPanelState() { + override val loading: Boolean + get() = departures.isNullOrEmpty() + + data class DeparturePlatforms( + val platform: String, + val departures: List, + ) + + data class DepartureInfo( + val routeName: String, + val routeColour: Color?, + val headsign: String, + val description: String?, + val time: Instant, + ) +} @Composable -internal fun StopInfoPanel( - state: InfoPanelState.Stop, - onEvent: (MapScreenEvent) -> Unit, +private fun listColors() = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + selectedContainerColor = MaterialTheme.colorScheme.primary, + selectedContentColor = MaterialTheme.colorScheme.onPrimary, +) + +@Composable +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +private fun MonoPlatform( + state: StopInfoPanelState.DeparturePlatforms ) { - Column(Modifier.fillMaxWidth()) { - Text( - state.name, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, - textAlign = TextAlign.Start - ) - state.subname?.let { - Text( - "/ $it", - modifier = Modifier.padding(start = 5.dp), - style = MaterialTheme.typography.titleSmall, - color = Color.Gray, - fontWeight = FontWeight.SemiBold, - textAlign = TextAlign.Start - ) - } - state.departures?.let { - Spacer(Modifier.height(5.dp)) - it.forEach { (name, formatted) -> - Row(verticalAlignment = Alignment.CenterVertically) { + val departures = state.departures + val lazyState = LazyListState(firstVisibleItemIndex = + departures.indexOfFirst { + it.time > Clock.System.now() + }.coerceAtLeast(0) + ) + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), + state = lazyState, + ) { + itemsIndexed(departures) { idx, dep -> + SegmentedListItem( + onClick = {}, + colors = listColors(), + shapes = ListItemDefaults.segmentedShapes( + idx, + departures.size, + ), + supportingContent = { + dep.description?.let { Text(dep.description) } + }, + trailingContent = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy((-4).dp) + ) { + Text( + text = (dep.time - Clock.System.now()).inWholeMinutes.toString(), + style = MaterialTheme.typography.headlineSmallEmphasized, + ) + Text( + text = "mn", + style = MaterialTheme.typography.labelSmallEmphasized, + ) + } + }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Box( + Modifier + .clip(ShapeDefaults.ExtraSmall) + .background(dep.routeColour ?: MaterialTheme.colorScheme.surface) + .padding(vertical = 2.dp, horizontal = 4.dp) + ) { + Text( + text = dep.routeName, + style = MaterialTheme.typography.labelSmallEmphasized, + color = MaterialTheme.colorScheme.surface, + ) + } Text( - name, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - Text( - formatted, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = 5.dp) + text = dep.headsign, + style = MaterialTheme.typography.labelLargeEmphasized, ) } } } } } + +@Composable +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +private fun ManyPlatforms( + state: List, +) { + val expandedList = remember { mutableStateListOf(*Array(state.size) { true }) } + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + state.forEachIndexed { idx, depInfo -> + val (platform, departures) = depInfo + val expanded = expandedList[idx] + stickyHeader(key = "header_${depInfo.hashCode()}") { + val base = ListItemDefaults.segmentedShapes(0, 2) + val large = MaterialTheme.shapes.large + + Box( + Modifier + .animateItem() + .background(MaterialTheme.colorScheme.surfaceContainerLow) + .padding(bottom = ListItemDefaults.SegmentedGap) + ) { + SegmentedListItem( + onClick = { expandedList[idx] = !expandedList[idx] }, + colors = listColors(), + shapes = if (expanded) base else base.copy(shape = large), + trailingContent = { + Icon( + painterResource(if (expanded) Res.drawable.arrow_drop_up else Res.drawable.arrow_drop_down), + contentDescription = null, + modifier = Modifier + .background( + if (expanded) MaterialTheme.colorScheme.surface else Color.Transparent, + shape = RoundedCornerShape(100) + ) + .padding(6.dp), + tint = MaterialTheme.colorScheme.onSurface, + ) + }, + ) { + Text( + text = platform, + style = MaterialTheme.typography.labelLarge, + ) + } + } + } + + if (expanded) { + item(key = "items_${depInfo.hashCode()}") { + Column( + modifier = Modifier.animateItem(), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), + ) { + departures.filter { it.time > Clock.System.now() }.take(5) + .forEachIndexed { idx, dep -> + SegmentedListItem( + onClick = {}, + colors = listColors(), + shapes = ListItemDefaults.segmentedShapes( + idx + 1, + (departures.size + 1).coerceAtMost(6), + ), + supportingContent = { + dep.description?.let { Text(dep.description) } + }, + trailingContent = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy((-4).dp) + ) { + Text( + text = (dep.time - Clock.System.now()).inWholeMinutes.toString(), + style = MaterialTheme.typography.headlineSmallEmphasized, + ) + Text( + text = "mn", + style = MaterialTheme.typography.labelSmallEmphasized, + ) + } + }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Box( + Modifier + .clip(ShapeDefaults.ExtraSmall) + .background( + dep.routeColour + ?: MaterialTheme.colorScheme.surface + ) + .padding(vertical = 2.dp, horizontal = 4.dp) + ) { + Text( + text = dep.routeName, + style = MaterialTheme.typography.labelSmallEmphasized, + color = MaterialTheme.colorScheme.surface, + ) + } + Text( + text = dep.headsign, + style = MaterialTheme.typography.labelLargeEmphasized, + ) + } + } + } + } + } + } + item(key = "spacer_${depInfo.hashCode()}") { + Spacer( + modifier = Modifier.animateItem().height(10.dp) + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +internal fun StopInfoPanel( + state: StopInfoPanelState, + onEvent: (StopInfoPanelEvent) -> Unit, +) { + val spec = fadeIn(tween(300, 300)) togetherWith fadeOut(tween(300)) + + AnimatedContent( + targetState = state, + contentKey = { it.id }, + transitionSpec = { spec }, + ) { state -> + Column(Modifier.fillMaxWidth().fillMaxHeight()) { + Row { + Column { + Text( + state.name, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Start + ) + state.subname?.let { + Text( + "/ $it", + modifier = Modifier.padding(start = 5.dp), + style = MaterialTheme.typography.titleSmall, + color = Color.Gray, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Start + ) + } + } + IconButton( + onClick = { onEvent(StopInfoPanelEvent.ToggleGrouping) }, + ) { Icon(Icons.Default.Edit, null) } + } + Spacer(Modifier.height(10.dp)) + AnimatedContent( + targetState = state.departures, + transitionSpec = { spec }, + ) { departures -> + departures?.let { departurePlatforms -> + if (departurePlatforms.size > 1) { + ManyPlatforms(departurePlatforms) + } else if (departurePlatforms.size == 1) { + MonoPlatform(departurePlatforms[0]) + } + } + } + } + } +} + +@Preview +@Composable +internal fun StopInfoPanelPreview() { + fun dateIn(dur: Duration) = (Clock.System.now() + dur) + + InfoPanel( + modifier = Modifier.background(BanksiaTheme.colors.background), + state = StopInfoPanelState( + id = "id", + name = "name", + subname = "sub", + departures = listOf( + StopInfoPanelState.DeparturePlatforms("Platform 1", listOf( + StopInfoPanelState.DepartureInfo("Sunbury", Color(TRAIN_BLUE), "Sunbury", "··· Malvern -> Anzac ··· Sunbury", dateIn(2.minutes)), + StopInfoPanelState.DepartureInfo("Sunbury", Color(TRAIN_BLUE), "West Footscray", "Express via Metro Tunnel", dateIn(8.minutes)), + )), + StopInfoPanelState.DeparturePlatforms("Platform 2", listOf( + StopInfoPanelState.DepartureInfo("237", Color(BUS_ORANGE), "Westall", null, dateIn(7.minutes)), + StopInfoPanelState.DepartureInfo("442", Color(BUS_ORANGE), "Dandenong", null, dateIn(8.minutes)), + )), + ), + ), + onEvent = {}, + onPeekHeightChange = {}, + ) +} diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/TripInfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/TripInfoPanel.kt index 2d221b2..29bdd37 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/TripInfoPanel.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/TripInfoPanel.kt @@ -9,14 +9,23 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import moe.lava.banksia.core.model.RouteType import moe.lava.banksia.ui.components.RouteIcon -import moe.lava.banksia.ui.screens.map.MapScreenEvent -import moe.lava.banksia.ui.state.InfoPanelState + +sealed class TripInfoPanelEvent : InfoPanelEvent() + +data class TripInfoPanelState( + val direction: String, + val type: RouteType, + val routeName: String? = null, +) : InfoPanelState() { + override val loading = routeName == null +} @Composable internal fun TripInfoPanel( - state: InfoPanelState.Trip, - onEvent: (MapScreenEvent) -> Unit, + state: TripInfoPanelState, + onEvent: (TripInfoPanelEvent) -> Unit, ) { Column(Modifier.fillMaxWidth()) { Row { diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt index 70b8ed4..1303bf5 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreen.kt @@ -38,9 +38,10 @@ import moe.lava.banksia.ui.layout.AppBottomSheet import moe.lava.banksia.ui.layout.Searcher import moe.lava.banksia.ui.layout.SheetStateWrapper import moe.lava.banksia.ui.layout.info.InfoPanel +import moe.lava.banksia.ui.layout.info.InfoPanelState import moe.lava.banksia.ui.map.Maps +import moe.lava.banksia.ui.map.rememberMapsPositionState import moe.lava.banksia.ui.platform.BanksiaTheme -import moe.lava.banksia.ui.state.InfoPanelState import org.jetbrains.compose.resources.painterResource import org.koin.compose.viewmodel.koinViewModel @@ -64,6 +65,13 @@ fun MapScreen( val sheetState = SheetStateWrapper.create() var searchExpandedState by rememberSaveable { mutableStateOf(false) } + val mapsPositionState = rememberMapsPositionState() + scope.launch { + viewModel.cameraChangeEmitter.collect { + mapsPositionState.update(it.value) + } + } + LaunchedEffect(infoState) { if (infoState !is InfoPanelState.None) { sheetState.peek() @@ -80,6 +88,7 @@ fun MapScreen( SearchBarDefaults.InputFieldHeight.roundToPx() }, bottom = sheetState.bottomInset), stops = mapState.stops, + positionState = mapsPositionState, // vehicles = mapState.vehicles, onStopClicked = { stop -> viewModel.handleEvent(MapScreenEvent.SelectStop(stop)) diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt index a3435af..de06381 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/MapScreenViewModel.kt @@ -15,25 +15,30 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant -import moe.lava.banksia.client.repository.RouteRepository -import moe.lava.banksia.client.repository.StopRepository -import moe.lava.banksia.client.repository.StopTimeRepository +import moe.lava.banksia.core.data.dto.ExtendedStopTime +import moe.lava.banksia.core.data.repositories.RouteRepository +import moe.lava.banksia.core.data.repositories.StopRepository +import moe.lava.banksia.core.data.repositories.StopTimeRepository +import moe.lava.banksia.core.model.Route +import moe.lava.banksia.core.model.RouteType +import moe.lava.banksia.core.util.BoxedValue +import moe.lava.banksia.core.util.BoxedValue.Companion.box +import moe.lava.banksia.core.util.LoopFlow.Companion.waitUntilSubscribed +import moe.lava.banksia.core.util.Point +import moe.lava.banksia.core.util.log import moe.lava.banksia.data.ptv.PtvService -import moe.lava.banksia.model.Route -import moe.lava.banksia.model.RouteType +import moe.lava.banksia.ui.extensions.getUIProperties +import moe.lava.banksia.ui.layout.info.InfoPanelEvent +import moe.lava.banksia.ui.layout.info.InfoPanelState +import moe.lava.banksia.ui.layout.info.RouteInfoPanelState +import moe.lava.banksia.ui.layout.info.StopInfoPanelEvent +import moe.lava.banksia.ui.layout.info.StopInfoPanelState +import moe.lava.banksia.ui.layout.info.TripInfoPanelState import moe.lava.banksia.ui.map.util.CameraPosition import moe.lava.banksia.ui.map.util.CameraPositionBounds import moe.lava.banksia.ui.map.util.Marker -import moe.lava.banksia.ui.state.InfoPanelState import moe.lava.banksia.ui.state.MapState import moe.lava.banksia.ui.state.SearchState -import moe.lava.banksia.util.BoxedValue -import moe.lava.banksia.util.BoxedValue.Companion.box -import moe.lava.banksia.util.LoopFlow.Companion.waitUntilSubscribed -import moe.lava.banksia.util.Point -import moe.lava.banksia.util.log -import kotlin.time.Clock -import kotlin.time.Duration.Companion.minutes sealed class MapScreenEvent { data object DismissState : MapScreenEvent() @@ -45,10 +50,13 @@ sealed class MapScreenEvent { data class SearchUpdate(val text: String) : MapScreenEvent() } -data class InternalState( +private data class InternalState( val route: String? = null, val stop: String? = null, val run: String? = null, + + val lastStopDepartures: List? = null, + val stopsGrouped: Boolean = true, ) class MapScreenViewModel( @@ -65,6 +73,10 @@ class MapScreenViewModel( viewModelScope.launch { switchRoute(value.route) } if (value.stop != last.stop) viewModelScope.launch { switchStop(value.stop) } + if (value.lastStopDepartures != last.lastStopDepartures) + viewModelScope.launch { buildDepartures() } + if (value.stopsGrouped != last.stopsGrouped) + viewModelScope.launch { buildDepartures() } if (value.run != last.run) switchRun(value.run) } @@ -99,6 +111,14 @@ class MapScreenViewModel( } } + fun handleEvent(event: InfoPanelEvent) { + viewModelScope.launch { + when (event) { + StopInfoPanelEvent.ToggleGrouping -> state = state.copy(stopsGrouped = !state.stopsGrouped) + } + } + } + fun bindTracker(locationTracker: LocationTracker) { locationTrackerJob = locationTracker.getLocationsFlow() .onEach { lastKnownLocation = Point(it.latitude, it.longitude) } @@ -106,11 +126,6 @@ class MapScreenViewModel( } fun centreCameraToLocation() { - viewModelScope.launch { - log("msvm", "getting..") - val routes = routeRepository.getAll() - log("msvm", routes.joinToString("\n")) - } lastKnownLocation?.let { location -> viewModelScope.launch { log("bvm", "emitting $location") @@ -160,9 +175,9 @@ class MapScreenViewModel( } val route = routeRepository.get(routeId) -// val gtfsRoute = ptvService.route(routeId) + ?: return iInfoState.update { - InfoPanelState.Route( + RouteInfoPanelState( name = route.name, type = route.type, ) @@ -187,7 +202,7 @@ class MapScreenViewModel( .onEach { run -> if (routeName == null) { iInfoState.update { - InfoPanelState.Trip( + TripInfoPanelState( direction = run.destinationName, type = RouteType.MetroTrain, // XXX HACK TODO FIXME ) @@ -196,7 +211,7 @@ class MapScreenViewModel( } iInfoState.update { - InfoPanelState.Trip( + TripInfoPanelState( direction = run.destinationName, type = RouteType.MetroTrain, // FIXME HACK XXX TODO routeName = routeName, @@ -210,45 +225,77 @@ class MapScreenViewModel( private suspend fun switchStop(id: String?) { if (id == null) { iInfoState.update { InfoPanelState.None } + state = state.copy(lastStopDepartures = null) return } val stop = stopRepository.get(id) -// val stop = ptvService.stop(routeType, stopId) val split = stop.name.split("/") val name = split[0] val subname = split.getOrNull(1) iInfoState.update { - InfoPanelState.Stop( + StopInfoPanelState( id = stop.id, name = name, subname = subname, ) } - val departures = stopTimeRepository.getForStop(id) - .filter { !it.headsign.isNullOrBlank() } - .groupBy { it.headsign!! } - .map { (headsign, stopTimes) -> - val now = Clock.System.now() - val times = stopTimes - .map { it.arrivalTime.toInstant(TimeZone.currentSystemDefault()) } - .filter { it >= (now - 1.minutes) } - .joinToString(" | ") { - val diff = (it - now).inWholeMinutes.coerceAtLeast(0) - if (diff >= 65) { - "${((diff + 30.0) / 60.0).toInt()}hr" - } else { - "${diff}mn" - } - } - InfoPanelState.Stop.Departure(headsign, times) + stopTimeRepository.getForStop(id) + .onEach { departures -> + state = state.copy( + lastStopDepartures = departures + ) + } + .launchIn(viewModelScope) + } + + private fun friendlyPlatform(platform: String) = + platform.takeUnless { it.firstOrNull()?.isDigit() == true } + ?: "Platform $platform" + private fun buildDepartures() { + val rawDepartures = state.lastStopDepartures ?: return + val departures = if (state.stopsGrouped) { + rawDepartures + .groupBy { it.stopPlatformCode } + .mapKeys { (platform) -> platform?.let { friendlyPlatform(it) } } + .entries + .sortedBy { (platform) -> platform } + .map { (platform, deps) -> + StopInfoPanelState.DeparturePlatforms( + platform = platform ?: "", + departures = deps.map { + StopInfoPanelState.DepartureInfo( + routeName = it.routeNumber ?: it.routeName, + routeColour = it.routeType.getUIProperties().colour, + headsign = it.headsign ?: it.routeName, + description = null, + time = it.time.departure.toInstant(TimeZone.currentSystemDefault()), + ) + } + ) + } + } else if (rawDepartures.isEmpty()) { + listOf() + } else { + listOf(StopInfoPanelState.DeparturePlatforms(platform = "", departures = rawDepartures.map { dep -> + StopInfoPanelState.DepartureInfo( + routeName = dep.routeNumber ?: dep.routeName, + routeColour = dep.routeType.getUIProperties().colour, + headsign = dep.headsign ?: dep.routeName, + description = dep.stopPlatformCode?.let { friendlyPlatform(it) }, + time = dep.time.departure.toInstant(TimeZone.currentSystemDefault()), + ) + })) + } + + departures.let { departures -> + iInfoState.update { + if (it !is StopInfoPanelState) + it + else + it.copy(departures = departures) } - iInfoState.update { - if (it !is InfoPanelState.Stop) - it - else - it.copy(departures = departures) } } diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/InfoPanelState.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/InfoPanelState.kt deleted file mode 100644 index 5b73914..0000000 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/InfoPanelState.kt +++ /dev/null @@ -1,38 +0,0 @@ -package moe.lava.banksia.ui.state - -import moe.lava.banksia.model.RouteType - -sealed class InfoPanelState { - abstract val loading: Boolean - - data object None : InfoPanelState() { - override val loading = false - } - - data class Route( - val name: String, - val type: RouteType, - ) : InfoPanelState() { - override val loading = false - } - - data class Stop( - val id: String, - val name: String, - val subname: String? = null, - val departures: List? = null, - ) : InfoPanelState() { - override val loading: Boolean - get() = departures == null - - data class Departure(val directionName: String, val formattedTimes: String) - } - - data class Trip( - val direction: String, - val type: RouteType, - val routeName: String? = null, - ) : InfoPanelState() { - override val loading = routeName == null - } -} diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt index 05429cb..9f60514 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/SearchState.kt @@ -1,6 +1,6 @@ package moe.lava.banksia.ui.state -import moe.lava.banksia.model.RouteType +import moe.lava.banksia.core.model.RouteType data class SearchState( val entries: List = listOf(),