diff --git a/.gitignore b/.gitignore index 426800f..83f099d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ captures secrets.properties shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt /data/ +/data diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts new file mode 100644 index 0000000..b8b100b --- /dev/null +++ b/androidApp/build.gradle.kts @@ -0,0 +1,57 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) +} + +kotlin { + target { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + compilerOptions { + freeCompilerArgs.add("-Xexplicit-backing-fields") + } + + dependencies { + implementation(projects.ui) + implementation(libs.androidx.activity.compose) + implementation(libs.compose.ui.tooling.preview) + } +} + +dependencies { + debugImplementation(libs.compose.ui.tooling) +} + +android { + namespace = "moe.lava.banksia" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + applicationId = "moe.lava.banksia" + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + signingConfig = signingConfigs.getByName("debug") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} diff --git a/ui/src/androidMain/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml similarity index 91% rename from ui/src/androidMain/AndroidManifest.xml rename to androidApp/src/main/AndroidManifest.xml index 928349e..16435e6 100644 --- a/ui/src/androidMain/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -13,9 +13,6 @@ android:enableOnBackInvokedCallback="true" android:usesCleartextTraffic="true" android:theme="@android:style/Theme.Material.Light.NoActionBar"> - { + 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 new file mode 100644 index 0000000..baf26e7 --- /dev/null +++ b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/stoptime/StopTimeRemoteDataSource.kt @@ -0,0 +1,36 @@ +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 new file mode 100644 index 0000000..8b46fbd --- /dev/null +++ b/client/src/commonMain/kotlin/moe/lava/banksia/client/data/trip/TripRemoteDataSource.kt @@ -0,0 +1,18 @@ +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/di/ClientModule.kt b/client/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt index a39a3ae..f22c7db 100644 --- a/client/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt +++ b/client/src/commonMain/kotlin/moe/lava/banksia/client/di/ClientModule.kt @@ -12,8 +12,11 @@ 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.data.ptv.PtvService import moe.lava.banksia.util.log import org.koin.core.module.dsl.singleOf @@ -46,8 +49,11 @@ val ClientModule = module { singleOf(::RouteRemoteDataSource) singleOf(::StopLocalDataSource) singleOf(::StopRemoteDataSource) + singleOf(::StopTimeLocalDataSource) + singleOf(::StopTimeRemoteDataSource) // Repositories singleOf(::RouteRepository) singleOf(::StopRepository) + singleOf(::StopTimeRepository) } 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 new file mode 100644 index 0000000..4f54840 --- /dev/null +++ b/client/src/commonMain/kotlin/moe/lava/banksia/client/repository/StopTimeRepository.kt @@ -0,0 +1,16 @@ +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/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..9b7b12a --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/536afcd1dff540251f85e5d2c80458cf/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/ecd23fd7707c683afbcd6052998cb6a9/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/536afcd1dff540251f85e5d2c80458cf/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e99bae143b75f9a10ead10248f02055e/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/04e088f8677de3b384108493cc9481d0/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/536afcd1dff540251f85e5d2c80458cf/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e55dccbfe27cb97945148c61a39c89c5/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/dbd05c4936d573642f94cd149e1356c8/redirect +toolchainVendor=JETBRAINS +toolchainVersion=21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 638a098..e171b55 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,40 +1,37 @@ [versions] -agp = "8.13.1" +agp = "9.1.0" android-compileSdk = "36" 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" +androidx-activity= "1.13.0" +androidx-lifecycle = "2.10.0" +compose-multiplatform = "1.11.0-alpha04" 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-alpha04" +okio = "3.17.0" playServicesLocation = "21.3.0" sqlite = "2.6.2" room = "2.8.4" secretsGradlePlugin = "2.0.1" -wire = "5.5.0" +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 +41,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,11 +63,11 @@ 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" } @@ -85,7 +75,7 @@ ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.r [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } -androidLibrary = { id = "com.android.library", version.ref = "agp" } +androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b..37f78a6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 2f7d989..39f75f3 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -12,8 +12,17 @@ application { applicationDefaultJvmArgs = listOf("-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}") } +kotlin { + compilerOptions { + freeCompilerArgs.add("-Xexplicit-backing-fields") + } +} + dependencies { implementation(projects.shared) + implementation(projects.server.gtfs) + implementation(projects.server.gtfsRt) + implementation(libs.logback) implementation(libs.koin.core) implementation(libs.koin.ktor) diff --git a/server/gtfs/build.gradle.kts b/server/gtfs/build.gradle.kts new file mode 100644 index 0000000..e83e745 --- /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.shared) + 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/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt similarity index 63% rename from server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt rename to server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt index d85d5df..21e239c 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt +++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt @@ -9,19 +9,26 @@ 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.Constants import moe.lava.banksia.model.Route +import moe.lava.banksia.model.RouteType +import moe.lava.banksia.model.Service +import moe.lava.banksia.model.ServiceException 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.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 @@ -29,19 +36,27 @@ 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( +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 StopTimeChunk(val stopTimes: List) : GtfsData() + data class TripChunk(val trips: List) : GtfsData() +} + +class GtfsParser( 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) { + suspend fun update(datasetUrl: String): Flow { val parentDir = datasetPath.parentFile @Suppress("SimplifyBooleanWithConstants", "KotlinConstantConditions") if (parentDir.exists() && !Constants.devMode) @@ -65,42 +80,65 @@ class GtfsHandler( .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) - addTrips(files) - addStopTimes(files) + log.info("parsing...") + return parse(files) + .onCompletion { + @Suppress("KotlinConstantConditions") + if (!Constants.devMode) { + parentDir.deleteRecursively() + } - updateMetadata(date ?: Clock.System.now().epochSeconds) - - @Suppress("KotlinConstantConditions") - if (!Constants.devMode) { - parentDir.deleteRecursively() - } - - log.info("done!") + 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 + private fun parse(files: List) = flow { + files .filter { it.name == "routes.txt" } - .flatMap { fd -> parseRoutes(fd) } + .forEach { emit(GtfsData.RouteChunk(parseRoutes(it))) } - log.info("inserting routes...") - dao.deleteAll() - dao.insertAll(*routes.map { it.asEntity() }.toTypedArray()) + files + .filter { it.name == "stops.txt" } + .forEach { emit(GtfsData.StopChunk(parseStops(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) + .also { emit(GtfsData.TripChunk(it)) } + } + .associateBy { it.id } + + files + .filter { it.name == "stop_times.txt" } + .forEach { fd -> + log.info("parsing stop times for ${fd.parent}...") + parseStopTimes(fd, trips) { seq -> + seq.chunked(10000) + .forEach { emit(GtfsData.StopTimeChunk(it)) } + } + } } private fun parseRoutes(fd: File) = @@ -108,24 +146,12 @@ class GtfsHandler( .map { with(it) { Route( id = route_id, - type = RouteTypeConverter.from(fd.parentFile.name.toInt()), + type = RouteType.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 } @@ -137,29 +163,6 @@ class GtfsHandler( 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) { @@ -174,27 +177,7 @@ class GtfsHandler( ) } } - private suspend fun addStopTimes(files: List) { - 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) { 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, block: (Sequence) -> Unit) = + private inline fun parseStopTimes(fd: File, trips: Map, block: (Sequence) -> Unit) = fd.parseCsvSequence { seq -> seq .map { with(it) { @@ -203,7 +186,7 @@ class GtfsHandler( stopId = stop_id, arrivalTime = GtfsStopTime.parseGtfsTime(arrival_time), departureTime = GtfsStopTime.parseGtfsTime(departure_time), - headsign = stop_headsign, + headsign = stop_headsign.ifEmpty { trips[trip_id]!!.tripHeadsign }, pickupType = pickup_type, dropOffType = drop_off_type, ) @@ -211,25 +194,43 @@ class GtfsHandler( .let { block(it) } } - private suspend fun addTrips(files: List) { - val dao = db.tripDao - log.info("parsing trips...") - val trips = files - .filter { it.name == "trips.txt" } - .flatMap { fd -> parseTrips(fd) } + 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), + ) + } } - log.info("inserting trips...") - dao.deleteAll() - dao.insertOrReplaceAll(*trips.map { it.asEntity() }.toTypedArray()) - } + private fun parseServiceExceptions(fd: File) = + fd.parseCsv() + .map { with(it) { + ServiceException( + serviceId = service_id, + date = LocalDate.parse(date, LocalDate.Formats.ISO_BASIC), + type = exception_type, + ) + } } - private fun parseTrips(fd: File) = + private fun parseTrips(fd: File, services: Map) = fd.parseCsv() .map { with(it) { Trip( id = trip_id, routeId = route_id, - serviceId = service_id, + service = services[service_id]!!, shapeId = shape_id.ifEmpty { null }, tripHeadsign = trip_headsign, directionId = direction_id, 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/gtfs/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 new file mode 100644 index 0000000..1bf9573 --- /dev/null +++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt @@ -0,0 +1,18 @@ +package moe.lava.banksia.server.gtfs.structures + +import kotlinx.serialization.Serializable + +@Suppress("PropertyName") +@Serializable +internal data class GtfsService( + val service_id: String, + val monday: Int, + val tuesday: Int, + val wednesday: Int, + val thursday: Int, + val friday: Int, + val saturday: Int, + val sunday: Int, + val start_date: String, + val end_date: String, +) 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 95% 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..76de3cd 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 @@ -5,7 +5,7 @@ import moe.lava.banksia.model.FutureTime @Suppress("PropertyName") @Serializable -data class GtfsStopTime( +internal data class GtfsStopTime( val trip_id: String, val arrival_time: String, val departure_time: String, 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..934d8bc --- /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.shared) + 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..30a9fd3 --- /dev/null +++ b/server/gtfs_rt/src/main/kotlin/moe/lava/banksia/server/gtfsrt/GtfsrtArchiver.kt @@ -0,0 +1,109 @@ +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.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 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(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) { + 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..8b30b2f --- /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.Constants +import moe.lava.banksia.util.LogScope +import moe.lava.banksia.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 94% 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..abebe76 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,4 +1,4 @@ -package moe.lava.banksia.data.gtfsr +package moe.lava.banksia.server.gtfsrt import com.google.transit.realtime.FeedMessage import moe.lava.banksia.util.Point 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 4ae3398..2d33793 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt +++ b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt @@ -15,17 +15,23 @@ 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.server.di.ServerModules -import moe.lava.banksia.server.gtfs.GtfsHandler -import moe.lava.banksia.server.gtfsr.GtfsrService +import moe.lava.banksia.server.gtfsrt.GtfsrtService +import moe.lava.banksia.util.serialise import org.koin.dsl.module import org.koin.ktor.ext.inject import org.koin.ktor.plugin.Koin +import kotlin.time.Clock fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module) @@ -41,10 +47,20 @@ fun Application.module() { modules(CommonModules, ServerModules) } - val gtfsr by inject() - launch { gtfsr.start() } + @Suppress("KotlinConstantConditions") + if (!Constants.devMode) { + val gtfsr by inject() + launch { gtfsr.start(this, true) } + } routing { + if (Constants.devMode) { + get("/fixup") { + call.respondText("received") + val fixer by inject() + fixer.addParentsToStops() + } + } get("/update") { val key = call.parameters["key"] if (key != Constants.updateKey) { @@ -57,8 +73,11 @@ 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) + val fixer by inject() + val importer by inject() + importer.import(datasetUrl) + + fixer.addParentsToStops() } } @@ -114,7 +133,7 @@ fun Application.module() { } 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) @@ -123,20 +142,23 @@ fun Application.module() { routeDao.stops(routeId) } 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..d3d307e --- /dev/null +++ b/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt @@ -0,0 +1,43 @@ +package moe.lava.banksia.server + +import moe.lava.banksia.room.Database +import moe.lava.banksia.room.entity.StopEntity +import moe.lava.banksia.util.log +import java.security.MessageDigest + +class GtfsDataFixer( + private val database: Database, +) { + suspend fun addParentsToStops() { + val dao = database.stopDao + val stops = dao.getAllParentless() + 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 = StopEntity( + id = parentId, + name = name, + lat = avgLat, + lng = avgLng, + parent = "", + hasWheelChairBoarding = stops.all { it.hasWheelChairBoarding }, + level = "", + platformCode = "", + ) + log("datafixer", "inserting ${parentId} for ${stops.size} children") + dao.insertAll(parent) + database.stopDao.updateParents(stops.map { it.id }, parentId) + } + } +} + +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..04ea373 --- /dev/null +++ b/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt @@ -0,0 +1,118 @@ +package moe.lava.banksia.server + +import androidx.room.immediateTransaction +import androidx.room.useWriterConnection +import io.ktor.util.logging.Logger +import moe.lava.banksia.model.Route +import moe.lava.banksia.model.Service +import moe.lava.banksia.model.ServiceException +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.entity.asEntity +import moe.lava.banksia.server.gtfs.GtfsData +import moe.lava.banksia.server.gtfs.GtfsParser +import kotlin.time.Clock + +class GtfsImporter( + private val parser: GtfsParser, + private val database: Database, + private val log: Logger, +) { + suspend fun import(url: String, date: Long = Clock.System.now().epochSeconds) { + database.useWriterConnection { transactor -> + transactor.immediateTransaction { + database.routeDao.deleteAll() + database.serviceDao.deleteAll() + database.serviceExceptionDao.deleteAll() + database.shapeDao.deleteAll() + database.stopDao.deleteAll() + database.stopTimeDao.deleteAll() + database.tripDao.deleteAll() + + parser.update(url).collect { chunk -> + when (chunk) { + is GtfsData.RouteChunk -> addRoutes(chunk.routes) + is GtfsData.ServiceChunk -> addServices(chunk.services) + is GtfsData.ServiceExceptionChunk -> addServiceExceptions(chunk.exceptions) + is GtfsData.ShapeChunk -> addShapes(chunk.shapes) + is GtfsData.StopChunk -> addStops(chunk.stops) + is GtfsData.StopTimeChunk -> addStopTimes(chunk.stopTimes) + is GtfsData.TripChunk -> addTrips(chunk.trips) + } + } + + updateMetadata(date) + } + } + } + + private suspend fun updateMetadata(date: Long) { + val dao = database.versionMetadataDao + log.info("updating metadata...") + dao.update(date, listOf("routes", "stops", "shapes", "trips", "stop_times")) + log.info("done") + } + + private suspend fun addRoutes(routes: List) { + val dao = database.routeDao + log.info("inserting routes...") + dao.insertOrReplaceAll(*routes.map { it.asEntity() }.toTypedArray()) + log.info("done") + } + + private suspend fun addServices(services: List) { + val dao = database.serviceDao + log.info("inserting services...") + dao.insertOrReplaceAll(*services.map { it.asEntity() }.toTypedArray()) + log.info("done") + } + + private suspend fun addServiceExceptions(exceptions: List) { + val dao = database.serviceExceptionDao + log.info("inserting exceptions...") + dao.insertOrReplaceAll(*exceptions.map { it.asEntity() }.toTypedArray()) + log.info("done") + } + + private suspend fun addShapes(shapes: List) { + val dao = database.shapeDao + log.info("inserting shapes...") + dao.insertOrReplaceAll(*shapes.map { it.asEntity() }.toTypedArray()) + log.info("done") + } + + private suspend fun addStops(stops: List) { + val dao = database.stopDao + 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") + } + } + } + } + dao.insertOrReplaceAll(*stops.map { it.asEntity() }.toTypedArray()) + log.info("done") + } + + private suspend fun addStopTimes(stopTimes: List) { + val dao = database.stopTimeDao + log.info("inserting ${stopTimes.size} stoptimes...") + dao.insertOrReplaceAll(*stopTimes.map { it.asEntity() }.toTypedArray()) + log.info("done") + } + + private suspend fun addTrips(trips: List) { + val dao = database.tripDao + log.info("inserting ${trips.size} trips...") + dao.insertOrReplaceAll(*trips.map { it.asEntity() }.toTypedArray()) + 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..6ee4365 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,18 @@ 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.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.singleOf import org.koin.dsl.module val ServerModules = module { single { HttpClient() } - singleOf(::GtfsHandler) - singleOf(::GtfsrService) + singleOf(::GtfsParser) + singleOf(::GtfsrtService) + + singleOf(::GtfsDataFixer) + singleOf(::GtfsImporter) } 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/settings.gradle.kts b/settings.gradle.kts index a33c5ec..72f0696 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} dependencyResolutionManagement { repositories { @@ -28,7 +31,12 @@ dependencyResolutionManagement { } } +include(":androidApp") include(":client") include(":server") +include(":server:gtfs") +include(":server:gtfs_rt") include(":shared") include(":ui") +include(":ui:maps") +include(":ui:shared") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 1f26a53..b4ed8ad 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,13 +1,11 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinSerialization) - alias(libs.plugins.androidLibrary) + alias(libs.plugins.androidMultiplatformLibrary) alias(libs.plugins.ksp) alias(libs.plugins.room) - alias(libs.plugins.wire) } room { @@ -15,8 +13,10 @@ room { } kotlin { - androidTarget { - @OptIn(ExperimentalKotlinGradlePluginApi::class) + android { + namespace = "moe.lava.banksia.shared" + compileSdk = libs.versions.android.compileSdk.get().toInt() + compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } @@ -26,7 +26,6 @@ kotlin { freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") } - iosX64() iosArm64() iosSimulatorArm64() @@ -58,27 +57,7 @@ kotlin { dependencies { add("kspAndroid", libs.room.compiler) - add("kspIosX64", libs.room.compiler) add("kspIosArm64", libs.room.compiler) add("kspIosSimulatorArm64", libs.room.compiler) add("kspJvm", libs.room.compiler) } - -android { - namespace = "moe.lava.banksia.shared" - compileSdk = libs.versions.android.compileSdk.get().toInt() - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() - } -} - -wire { - sourcePath { - srcDir("src/commonMain/proto") - } - kotlin {} -} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/10.json b/shared/schemas/moe.lava.banksia.room.Database/10.json new file mode 100644 index 0000000..751e946 --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/10.json @@ -0,0 +1,477 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "5b90bc800bfae6d22124ea0a6a906ca7", + "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": "ServiceException", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serviceId` TEXT NOT NULL, `date` INTEGER NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`serviceId`, `date`))", + "fields": [ + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serviceId", + "date" + ] + }, + "indices": [ + { + "name": "index_ServiceException_serviceId", + "unique": false, + "columnNames": [ + "serviceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ServiceException_serviceId` ON `${TABLE_NAME}` (`serviceId`)" + }, + { + "name": "index_ServiceException_type", + "unique": false, + "columnNames": [ + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ServiceException_type` ON `${TABLE_NAME}` (`type`)" + } + ] + }, + { + "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, '5b90bc800bfae6d22124ea0a6a906ca7')" + ] + } +} \ No newline at end of file diff --git a/shared/schemas/moe.lava.banksia.room.Database/11.json b/shared/schemas/moe.lava.banksia.room.Database/11.json new file mode 100644 index 0000000..6fc2976 --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/11.json @@ -0,0 +1,498 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "c4be3d0c2a25f8c5c33132646a070d0e", + "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": "ServiceException", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serviceId` TEXT NOT NULL, `date` INTEGER NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`serviceId`, `date`))", + "fields": [ + { + "fieldPath": "serviceId", + "columnName": "serviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serviceId", + "date" + ] + }, + "indices": [ + { + "name": "index_ServiceException_serviceId", + "unique": false, + "columnNames": [ + "serviceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ServiceException_serviceId` ON `${TABLE_NAME}` (`serviceId`)" + }, + { + "name": "index_ServiceException_type", + "unique": false, + "columnNames": [ + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ServiceException_type` ON `${TABLE_NAME}` (`type`)" + } + ] + }, + { + "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, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`parent`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED)", + "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" + }, + { + "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`)" + } + ], + "foreignKeys": [ + { + "table": "Stop", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "parent" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "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_serviceId", + "unique": false, + "columnNames": [ + "serviceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Trip_serviceId` ON `${TABLE_NAME}` (`serviceId`)" + }, + { + "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, 'c4be3d0c2a25f8c5c33132646a070d0e')" + ] + } +} \ 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 new file mode 100644 index 0000000..783b3ee --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/4.json @@ -0,0 +1,368 @@ +{ + "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 new file mode 100644 index 0000000..c4a786d --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/5.json @@ -0,0 +1,368 @@ +{ + "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 new file mode 100644 index 0000000..5ab26dc --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/6.json @@ -0,0 +1,368 @@ +{ + "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 new file mode 100644 index 0000000..d4c62b2 --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/7.json @@ -0,0 +1,415 @@ +{ + "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 new file mode 100644 index 0000000..9240dd5 --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/8.json @@ -0,0 +1,426 @@ +{ + "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 new file mode 100644 index 0000000..2359dbd --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/9.json @@ -0,0 +1,426 @@ +{ + "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/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton b/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton index 7329ae3..15b3c58 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/Constants.kt.skeleton @@ -8,4 +8,5 @@ object Constants { // TODO const val devMode: Boolean = false const val updateKey: String = "" + const val protomapsKey: String = "" } diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt index 0726665..c9988bf 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRouteType.kt @@ -9,7 +9,7 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import moe.lava.banksia.model.RouteType -private object PtvRouteTypeSerialiser : KSerializer { +object PtvRouteTypeSerialiser : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( PtvRouteType::class.qualifiedName!!, PrimitiveKind.INT) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt index 823174b..1a39cfb 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt @@ -9,6 +9,8 @@ val CommonModules = module { single { Database.build(get().getBuilder()) } single { get().versionMetadataDao } single { get().routeDao } + single { get().serviceDao } + single { get().serviceExceptionDao } single { get().shapeDao } single { get().stopDao } single { get().stopTimeDao } diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt index c1853a9..91c5c77 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/FutureTime.kt @@ -1,6 +1,10 @@ package moe.lava.banksia.model +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalTime +import kotlinx.datetime.atTime +import kotlinx.datetime.plus import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -39,6 +43,10 @@ data class FutureTime( val minute = time.minute val second = time.second val trueHour = time.hour + (if (dayOffset) 24 else 0) + + fun atDate(date: LocalDate) = date + .let { if (dayOffset) date.plus(1, DateTimeUnit.DAY) else date } + .atTime(time) } object FutureTimeSerialiser: KSerializer { diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt index 08a9c53..a51f132 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt @@ -13,4 +13,8 @@ enum class RouteType(val value: Int) { SkyBus(11), Interstate(10), ; + + companion object { + fun from(value: Int) = RouteType.entries.first { it.value == value } + } } diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/ServiceException.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/ServiceException.kt new file mode 100644 index 0000000..305ede4 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/ServiceException.kt @@ -0,0 +1,11 @@ +package moe.lava.banksia.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/Stop.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt index df10a58..e1060bb 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Stop.kt @@ -8,7 +8,7 @@ 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, diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTimeDated.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTimeDated.kt new file mode 100644 index 0000000..55288fa --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/StopTimeDated.kt @@ -0,0 +1,26 @@ +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 index ef95eea..81d3f8d 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Trip.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable data class Trip( val id: String, val routeId: String, - val serviceId: String, + val service: Service, val shapeId: String?, val tripHeadsign: String, val directionId: 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 index 163461a..7c39ebf 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt @@ -3,17 +3,25 @@ package moe.lava.banksia.room import androidx.room.AutoMigration import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.room.migration.Migration +import androidx.room.util.foreignKeyCheck +import androidx.sqlite.SQLiteConnection import androidx.sqlite.driver.bundled.BundledSQLiteDriver +import androidx.sqlite.execSQL import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import moe.lava.banksia.room.converter.RouteTypeConverter -import moe.lava.banksia.room.dao.VersionMetadataDao import moe.lava.banksia.room.dao.RouteDao +import moe.lava.banksia.room.dao.ServiceDao +import moe.lava.banksia.room.dao.ServiceExceptionDao 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.ServiceExceptionEntity import moe.lava.banksia.room.entity.ShapeEntity import moe.lava.banksia.room.entity.StopEntity import moe.lava.banksia.room.entity.StopTimeEntity @@ -22,9 +30,11 @@ import moe.lava.banksia.room.entity.VersionMetadataEntity import androidx.room.Database as DatabaseAnnotation @DatabaseAnnotation( - version = 3, + version = 11, entities = [ RouteEntity::class, + ServiceEntity::class, + ServiceExceptionEntity::class, ShapeEntity::class, StopEntity::class, StopTimeEntity::class, @@ -34,12 +44,15 @@ import androidx.room.Database as DatabaseAnnotation autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), + AutoMigration(from = 9, to = 10), ] ) @TypeConverters(RouteTypeConverter::class) abstract class Database : RoomDatabase() { abstract val versionMetadataDao: VersionMetadataDao abstract val routeDao: RouteDao + abstract val serviceDao: ServiceDao + abstract val serviceExceptionDao: ServiceExceptionDao abstract val shapeDao: ShapeDao abstract val stopDao: StopDao abstract val stopTimeDao: StopTimeDao @@ -50,7 +63,21 @@ abstract class Database : RoomDatabase() { base.fallbackToDestructiveMigration(true) .setDriver(BundledSQLiteDriver()) .setQueryCoroutineContext(Dispatchers.IO) + .addMigrations(MIGRATION_10_11) // .fallbackToDestructiveMigration(true) .build() } } + +val MIGRATION_10_11 = object : Migration(10, 11) { + override fun migrate(connection: SQLiteConnection) { + connection.execSQL("CREATE TABLE IF NOT EXISTS `_new_Stop` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `parent` TEXT, `hasWheelChairBoarding` INTEGER NOT NULL, `level` TEXT NOT NULL, `platformCode` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`parent`) REFERENCES `Stop`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED)") + connection.execSQL("INSERT INTO `_new_Stop` (`id`,`name`,`lat`,`lng`,`parent`,`hasWheelChairBoarding`,`level`,`platformCode`) SELECT `id`,`name`,`lat`,`lng`,`parent`,`hasWheelChairBoarding`,`level`,`platformCode` FROM `Stop`") + connection.execSQL("UPDATE `_new_Stop` SET `parent` = NULL WHERE `parent` == \"\"") + connection.execSQL("DROP TABLE `Stop`") + connection.execSQL("ALTER TABLE `_new_Stop` RENAME TO `Stop`") + connection.execSQL("CREATE INDEX IF NOT EXISTS `index_Stop_parent` ON `Stop` (`parent`)") + connection.execSQL("CREATE INDEX IF NOT EXISTS `index_Trip_serviceId` ON `Trip` (`serviceId`)") + foreignKeyCheck(connection, "Stop") + } +} 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 index 8927f14..9ceb612 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/RouteTypeConverter.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/RouteTypeConverter.kt @@ -5,7 +5,7 @@ import moe.lava.banksia.model.RouteType object RouteTypeConverter { @TypeConverter - fun from(value: Int) = RouteType.entries.first { it.value == value } + fun from(value: Int) = RouteType.from(value) @TypeConverter fun to(routeType: RouteType) = routeType.value 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 index 0174f0f..94ce892 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt @@ -37,13 +37,22 @@ interface RouteDao { """) suspend fun stops(id: String): List + // I vibecoded this, sorry @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 + WITH Tree AS ( + 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 + + UNION ALL + + SELECT s.* + FROM Stop s + INNER JOIN Tree t ON s.id = t.parent + ) + SELECT DISTINCT * FROM Tree WHERE parent IS NULL; """) 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 new file mode 100644 index 0000000..6fc2906 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ServiceDao.kt @@ -0,0 +1,29 @@ +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/ServiceExceptionDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ServiceExceptionDao.kt new file mode 100644 index 0000000..123b0c6 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ServiceExceptionDao.kt @@ -0,0 +1,29 @@ +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.ServiceExceptionEntity + +@Dao +interface ServiceExceptionDao { + @Query("SELECT * FROM ServiceException") + suspend fun getAll(): List + + @Query("SELECT * FROM ServiceException WHERE serviceId == :id") + suspend fun get(id: String): List + + @Insert + suspend fun insertAll(vararg exceptions: ServiceExceptionEntity) + + @Insert(onConflict = REPLACE) + suspend fun insertOrReplaceAll(vararg exceptions: ServiceExceptionEntity) + + @Delete + suspend fun delete(service: ServiceExceptionEntity) + + @Query("DELETE FROM ServiceException") + 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 index c48735a..ae4d53a 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt @@ -3,6 +3,7 @@ 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.ShapeEntity @@ -14,6 +15,9 @@ interface ShapeDao { @Insert suspend fun insertAll(vararg shapes: ShapeEntity) + @Insert(onConflict = REPLACE) + suspend fun insertOrReplaceAll(vararg shapes: ShapeEntity) + @Delete suspend fun delete(shape: ShapeEntity) 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 index f6b2ef2..869ae29 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopDao.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopDao.kt @@ -12,6 +12,13 @@ interface StopDao { @Query("SELECT * FROM Stop") suspend fun getAll(): List + @Query(""" + SELECT * FROM Stop + WHERE platformCode <> "" + AND parent == "" + """) + suspend fun getAllParentless(): List + @Query("SELECT * FROM Stop WHERE id == :id") suspend fun get(id: String): StopEntity? @@ -29,4 +36,7 @@ interface StopDao { @Query("DELETE FROM Stop") suspend fun deleteAll() + + @Query("UPDATE Stop SET parent = :parent WHERE id IN (:ids)") + suspend fun updateParents(ids: List, parent: String) } 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 index 88485f4..82e0e4b 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt @@ -13,10 +13,24 @@ interface StopTimeDao { suspend fun getAll(): List @Query("SELECT * FROM StopTime WHERE tripId == :tripId") - suspend fun get(tripId: String): StopTimeEntity? + suspend fun getForTrip(tripId: String): StopTimeEntity? @Query("SELECT * FROM StopTime WHERE tripId IN (:tripIds)") - suspend fun get(tripIds: List): List + suspend fun getForTrips(tripIds: List): List + + @Query("SELECT * FROM StopTime WHERE stopId == :stopId") + suspend fun getForStop(stopId: String): List + + @Query(""" + SELECT DISTINCT StopTime.* 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 + LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date + WHERE StopTime.tripId == Trip.id + AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId) + AND ServiceException.type IS NULL + """) + suspend fun getForStopDated(stopId: String, days: Int, date: Int): List @Insert suspend fun insertAll(vararg stopTimes: StopTimeEntity) 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 index 4b14a95..027aaa8 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceEntity.kt @@ -1,50 +1,23 @@ package moe.lava.banksia.room.entity -import kotlinx.datetime.DayOfWeek +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( - val id: String, - val days: Int, + @PrimaryKey val id: String, + @ColumnInfo(index = true) val days: Int, val start: Int, val end: Int, ) { - object Parser { - private fun Int.check(other: Int) = (this and other) != 0 - - fun deserialiseDays(days: Int): List = buildList { - if (days.check(1)) - add(DayOfWeek.MONDAY) - if (days.check(1 shl 1)) - add(DayOfWeek.TUESDAY) - if (days.check(1 shl 2)) - add(DayOfWeek.WEDNESDAY) - if (days.check(1 shl 3)) - add(DayOfWeek.THURSDAY) - if (days.check(1 shl 4)) - add(DayOfWeek.FRIDAY) - if (days.check(1 shl 5)) - add(DayOfWeek.SATURDAY) - if (days.check(1 shl 6)) - add(DayOfWeek.SUNDAY) - } - fun serialiseDays(days: List): Int = - days.fold(0) { vl, n -> - vl + when (n) { - DayOfWeek.MONDAY -> 1 - DayOfWeek.TUESDAY -> 1 shl 1 - DayOfWeek.WEDNESDAY -> 1 shl 2 - DayOfWeek.THURSDAY -> 1 shl 3 - DayOfWeek.FRIDAY -> 1 shl 4 - DayOfWeek.SATURDAY -> 1 shl 5 - DayOfWeek.SUNDAY -> 1 shl 6 - } - } - } fun asModel() = Service( id, - Parser.deserialiseDays(days), + days.deserialiseDaysBitflag(), LocalDate.fromEpochDays(start), LocalDate.fromEpochDays(end), ) @@ -52,7 +25,7 @@ data class ServiceEntity( fun Service.asEntity() = ServiceEntity( id, - ServiceEntity.Parser.serialiseDays(days), + days.serialise(), start.toEpochDays().toInt(), end.toEpochDays().toInt(), ) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceExceptionEntity.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceExceptionEntity.kt new file mode 100644 index 0000000..313246d --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/ServiceExceptionEntity.kt @@ -0,0 +1,28 @@ +package moe.lava.banksia.room.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import kotlinx.datetime.LocalDate +import moe.lava.banksia.model.ServiceException + +@Entity( + "ServiceException", + primaryKeys = ["serviceId", "date"] +) +data class ServiceExceptionEntity( + @ColumnInfo(index = true) val serviceId: String, + val date: Int, + @ColumnInfo(index = true) val type: Int, +) { + fun asModel() = ServiceException( + serviceId, + LocalDate.fromEpochDays(date), + type, + ) +} + +fun ServiceException.asEntity() = ServiceExceptionEntity( + serviceId, + date.toEpochDays().toInt(), + type, +) 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 index 9c6cf15..9ce7bfb 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopEntity.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopEntity.kt @@ -2,17 +2,30 @@ package moe.lava.banksia.room.entity import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.Companion.SET_NULL import androidx.room.PrimaryKey import moe.lava.banksia.model.Stop import moe.lava.banksia.util.Point -@Entity("Stop") +@Entity( + "Stop", + foreignKeys = [ + ForeignKey( + StopEntity::class, + parentColumns = ["id"], + childColumns = ["parent"], + onDelete = SET_NULL, + deferred = true, + ), + ] +) data class StopEntity( @PrimaryKey val id: String, val name: String, val lat: Double, val lng: Double, - @ColumnInfo(index = true) val parent: String, + @ColumnInfo(index = true) val parent: String?, val hasWheelChairBoarding: Boolean, val level: String, val platformCode: String, 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 index 9b0aac8..bb20ff1 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopTimeEntity.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/StopTimeEntity.kt @@ -3,6 +3,7 @@ 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 @@ -11,6 +12,10 @@ 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), 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 index ca7e9a7..12bda02 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/entity/TripEntity.kt @@ -4,6 +4,7 @@ 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 @@ -11,8 +12,10 @@ import moe.lava.banksia.model.Trip "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"), Index("serviceId")], ) data class TripEntity( @PrimaryKey val id: String, @@ -23,8 +26,24 @@ data class TripEntity( val directionId: String, val blockId: String, val wheelchairAccessible: String, -) { - fun asModel() = Trip(id, routeId, serviceId, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible) +) + +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, serviceId, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible) +fun Trip.asEntity() = TripEntity(id, routeId, service.id, shapeId, tripHeadsign, directionId, blockId, wheelchairAccessible) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt index 0d6896d..3ff5702 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/util/BoxedValue.kt @@ -1,5 +1,6 @@ package moe.lava.banksia.util +/** Wraps an arbitrary value, such that equality checks are forced to be done by reference */ class BoxedValue(val value: T) { operator fun component1() = value diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/util/DayOfWeekExtension.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/util/DayOfWeekExtension.kt new file mode 100644 index 0000000..87d3244 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/util/DayOfWeekExtension.kt @@ -0,0 +1,36 @@ +package moe.lava.banksia.util + +import kotlinx.datetime.DayOfWeek + +private fun Int.check(other: Int) = (this and other) != 0 + +fun Int.deserialiseDaysBitflag(): List = buildList { + val days = this@deserialiseDaysBitflag + if (days.check(1)) + add(DayOfWeek.MONDAY) + if (days.check(1 shl 1)) + add(DayOfWeek.TUESDAY) + if (days.check(1 shl 2)) + add(DayOfWeek.WEDNESDAY) + if (days.check(1 shl 3)) + add(DayOfWeek.THURSDAY) + if (days.check(1 shl 4)) + add(DayOfWeek.FRIDAY) + if (days.check(1 shl 5)) + add(DayOfWeek.SATURDAY) + if (days.check(1 shl 6)) + add(DayOfWeek.SUNDAY) +} + +fun List.serialise(): Int = + this.fold(0) { vl, n -> + vl + when (n) { + DayOfWeek.MONDAY -> 1 + DayOfWeek.TUESDAY -> 1 shl 1 + DayOfWeek.WEDNESDAY -> 1 shl 2 + DayOfWeek.THURSDAY -> 1 shl 3 + DayOfWeek.FRIDAY -> 1 shl 4 + DayOfWeek.SATURDAY -> 1 shl 5 + DayOfWeek.SUNDAY -> 1 shl 6 + } + } diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index 100228c..201a10f 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -1,25 +1,31 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinSerialization) - alias(libs.plugins.androidApplication) + alias(libs.plugins.androidMultiplatformLibrary) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) alias(libs.plugins.secretsGradle) } kotlin { - androidTarget { - @OptIn(ExperimentalKotlinGradlePluginApi::class) + android { + namespace = "moe.lava.banksia.ui" + compileSdk = libs.versions.android.compileSdk.get().toInt() + compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } + + androidResources { + enable = true + } } compilerOptions { freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + freeCompilerArgs.add("-Xexplicit-backing-fields") } listOf( @@ -35,9 +41,6 @@ kotlin { sourceSets { androidMain.dependencies { - implementation(libs.compose.ui.tooling.preview) - implementation(libs.androidx.activity.compose) - implementation(libs.kotlinx.coroutines.android) implementation(libs.play.services.location) } commonMain.dependencies { @@ -66,47 +69,16 @@ kotlin { implementation(projects.client) implementation(projects.shared) + implementation(projects.ui.maps) + implementation(projects.ui.shared) } } } -android { - namespace = "moe.lava.banksia" - compileSdk = libs.versions.android.compileSdk.get().toInt() - - defaultConfig { - applicationId = "moe.lava.banksia" - minSdk = libs.versions.android.minSdk.get().toInt() - targetSdk = libs.versions.android.targetSdk.get().toInt() - versionCode = 1 - versionName = "1.0" - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } - buildTypes { - getByName("release") { - isMinifyEnabled = false - signingConfig = signingConfigs.getByName("debug") - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - dependencies { - debugImplementation(compose.uiTooling) + androidRuntimeClasspath(libs.compose.ui.tooling) } secrets { propertiesFileName = "secrets.properties" } - -compose.resources { - publicResClass = true - packageOfResClass = "moe.lava.banksia.resources" -} diff --git a/ui/maps/build.gradle.kts b/ui/maps/build.gradle.kts new file mode 100644 index 0000000..324b0b3 --- /dev/null +++ b/ui/maps/build.gradle.kts @@ -0,0 +1,56 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) +} + +kotlin { + android { + namespace = "moe.lava.banksia.ui.map" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + freeCompilerArgs.add("-Xexplicit-backing-fields") + } + + iosArm64() + iosSimulatorArm64() + + sourceSets { + androidMain.dependencies { + implementation(libs.compose.ui.tooling.preview) + implementation(libs.androidx.activity.compose) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.play.services.location) + } + commonMain.dependencies { + implementation(libs.koin.core) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) + implementation(libs.ktor.serialization.kotlinx.json) + + implementation(libs.maplibre.compose) + implementation(libs.moko.geo) + implementation(libs.moko.geo.compose) + + implementation(libs.compose.components.resources) + implementation(libs.compose.runtime) + implementation(libs.compose.foundation) + implementation(libs.compose.material3) + implementation(libs.compose.ui) + + implementation(projects.shared) + 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 new file mode 100644 index 0000000..1df9cb1 --- /dev/null +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapLibreMaps.kt @@ -0,0 +1,85 @@ +package moe.lava.banksia.ui.map + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.WindowInsets +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.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlinx.serialization.json.JsonObject +import moe.lava.banksia.Constants +import moe.lava.banksia.ui.map.mappers.routeColorExpression +import moe.lava.banksia.ui.platform.BanksiaTheme +import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.camera.rememberCameraState +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.layers.CircleLayer +import org.maplibre.compose.map.MapOptions +import org.maplibre.compose.map.MaplibreMap +import org.maplibre.compose.map.OrnamentOptions +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.compose.style.BaseStyle +import org.maplibre.compose.util.ClickResult +import org.maplibre.spatialk.geojson.Feature +import org.maplibre.spatialk.geojson.Geometry + +@Composable +internal fun MapLibreMaps( + modifier: Modifier, + insets: WindowInsets, + positionState: MapsPositionState, + stops: GeoJsonData.Features?, +// vehicles: GeoJsonData.Features?, + stopInnerColor: Color, + onStopClicked: (Feature) -> Unit, +) { + val camPos = rememberCameraState( + CameraPosition( + zoom = 16.0, + target = MELBOURNE_POS + ) + ) + + val variant = if (isSystemInDarkTheme()) "dark" else "light" + + MaplibreMap( + modifier = modifier, + baseStyle = BaseStyle.Uri("https://api.protomaps.com/styles/v5/$variant/en.json?key=${Constants.protomapsKey}"), + cameraState = camPos, + options = MapOptions( + ornamentOptions = OrnamentOptions( + padding = WindowInsets.safeDrawing.add(insets).asPaddingValues(), + isScaleBarEnabled = false, + isAttributionEnabled = false, + ) + ) + ) { + if (stops != null) { + val stopsSource = rememberGeoJsonSource(stops) + CircleLayer( + id = "maps-stops0", + source = stopsSource, + color = const(BanksiaTheme.colors.surface), + radius = const(3.dp), + strokeWidth = const(2.dp), + strokeColor = routeColorExpression, + ) + CircleLayer( + id = "maps-stops0-clickhandler", + source = stopsSource, + color = const(Color.Transparent), + radius = const(12.dp), + onClick = { features -> +// onEvent(MapScreenEvent.SelectStop(marker.type to feature.id!!.content)) +// val marker = Json.decodeFromJsonElement(feature.properties!!) + onStopClicked(features[0]) + ClickResult.Consume + } + ) + } + } +} 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 new file mode 100644 index 0000000..52b7250 --- /dev/null +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/Maps.kt @@ -0,0 +1,37 @@ +package moe.lava.banksia.ui.map + +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.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() + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Maps( + modifier: Modifier = Modifier, + insets: WindowInsets = WindowInsets(), + stops: List = listOf(), +// vehicles: List = listOf(), + positionState: MapsPositionState = rememberMapsPositionState(), + onStopClicked: (id: String) -> Unit = {}, +// onVehicleClicked: (id: String) -> Unit = {}, +) { + MapLibreMaps( + modifier = modifier, + insets = insets, + positionState = positionState, + stops = stops.takeIf { it.isNotEmpty() }?.asFeatures(), +// vehicles = vehicles.takeIf { it.isNotEmpty() }?.asFeatures(), + stopInnerColor = BanksiaTheme.colors.surface, + onStopClicked = { feature -> onStopClicked(feature.id!!.content) }, +// onVehicleClicked = {}, + ) +} 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 new file mode 100644 index 0000000..a9fe8b2 --- /dev/null +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/MapsPositionState.kt @@ -0,0 +1,27 @@ +package moe.lava.banksia.ui.map + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch +import moe.lava.banksia.util.Point + +class MapsPositionState internal constructor( + private val scope: CoroutineScope +) { + internal val updates: SharedFlow + field = MutableSharedFlow() + + fun update(position: Point) { + scope.launch { updates.emit(position) } + } +} + +@Composable +fun rememberMapsPositionState(): MapsPositionState { + val scope = rememberCoroutineScope() + return remember { MapsPositionState(scope) } +} 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 new file mode 100644 index 0000000..32a910c --- /dev/null +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Marker.kt @@ -0,0 +1,40 @@ +package moe.lava.banksia.ui.map.mappers + +import kotlinx.serialization.Serializable +import moe.lava.banksia.model.RouteType +import moe.lava.banksia.ui.map.util.Marker +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.spatialk.geojson.FeatureCollection +import org.maplibre.spatialk.geojson.dsl.addFeature +import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection +import org.maplibre.spatialk.geojson.Point as MLPoint + +@Serializable +data class MarkerProps( + val type: RouteType, +) + +@Suppress("NOTHING_TO_INLINE") +internal inline fun Iterable.asFeatures() = GeoJsonData.Features(asFeatureCollection()) + +internal fun Iterable.asFeatureCollection(): FeatureCollection { + val markers = this + return buildFeatureCollection { + markers.forEach { marker -> + val type = when (marker) { + is Marker.Stop -> marker.type + is Marker.Vehicle -> marker.type + } + val id = when (marker) { + is Marker.Stop -> marker.id + is Marker.Vehicle -> marker.ref + } + addFeature( + geometry = MLPoint(marker.point.toPosition()), + properties = MarkerProps(type), + ) { + setId(id) + } + } + } +} 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 new file mode 100644 index 0000000..c137394 --- /dev/null +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/Position.kt @@ -0,0 +1,6 @@ +package moe.lava.banksia.ui.map.mappers + +import moe.lava.banksia.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 new file mode 100644 index 0000000..523e438 --- /dev/null +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/mappers/RouteType.kt @@ -0,0 +1,19 @@ +package moe.lava.banksia.ui.map.mappers + +import androidx.compose.runtime.Composable +import moe.lava.banksia.model.RouteType +import moe.lava.banksia.ui.extensions.getUIProperties +import moe.lava.banksia.ui.platform.BanksiaTheme +import org.maplibre.compose.expressions.dsl.case +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.convertToString +import org.maplibre.compose.expressions.dsl.feature +import org.maplibre.compose.expressions.dsl.switch + +internal val routeColorExpression @Composable get() = switch( + input = feature["type"].convertToString(), + cases = RouteType.entries.map { + case(label = it.name, output = const(it.getUIProperties().colour)) + }.toTypedArray(), + fallback = const(BanksiaTheme.colors.surface), +) diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPosition.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt similarity index 81% rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPosition.kt rename to ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt index 2bc80af..710cebb 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPosition.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPosition.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.ui.utils.map +package moe.lava.banksia.ui.map.util import moe.lava.banksia.util.Point diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPositionBounds.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt similarity index 74% rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPositionBounds.kt rename to ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt index 335f668..4adf3b1 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/CameraPositionBounds.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/CameraPositionBounds.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.ui.utils.map +package moe.lava.banksia.ui.map.util import moe.lava.banksia.util.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 new file mode 100644 index 0000000..9326b2a --- /dev/null +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Marker.kt @@ -0,0 +1,28 @@ +package moe.lava.banksia.ui.map.util + +import kotlinx.serialization.Serializable +import moe.lava.banksia.model.RouteType +import moe.lava.banksia.util.Point + +@Serializable +sealed class Marker { + abstract val point: Point + + sealed class Typed : Marker() { + abstract val type: RouteType + } + + @Serializable + data class Stop( + override val point: Point, + override val type: RouteType, + val id: String, + ) : Typed() + + @Serializable + data class Vehicle( + override val point: Point, + override val type: RouteType, + val ref: String, + ) : Typed() +} diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Polyline.kt b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt similarity index 79% rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Polyline.kt rename to ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt index d9529e4..146d74b 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Polyline.kt +++ b/ui/maps/src/commonMain/kotlin/moe/lava/banksia/ui/map/util/Polyline.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.ui.utils.map +package moe.lava.banksia.ui.map.util import androidx.compose.ui.graphics.Color import moe.lava.banksia.util.Point diff --git a/ui/shared/build.gradle.kts b/ui/shared/build.gradle.kts new file mode 100644 index 0000000..c784fed --- /dev/null +++ b/ui/shared/build.gradle.kts @@ -0,0 +1,50 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) +} + +kotlin { + android { + namespace = "moe.lava.banksia.ui.shared" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + freeCompilerArgs.add("-Xexplicit-backing-fields") + } + + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(libs.compose.components.resources) + implementation(libs.compose.runtime) + implementation(libs.compose.foundation) + implementation(libs.compose.material3) + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling.preview) + + implementation(projects.shared) + } + } +} + +dependencies { + androidRuntimeClasspath(libs.compose.ui.tooling) +} + +compose.resources { + publicResClass = true + packageOfResClass = "moe.lava.banksia.resources" +} diff --git a/ui/src/androidMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.android.kt b/ui/shared/src/androidMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.android.kt similarity index 100% rename from ui/src/androidMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.android.kt rename to ui/shared/src/androidMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.android.kt diff --git a/ui/src/commonMain/composeResources/drawable/bus.xml b/ui/shared/src/commonMain/composeResources/drawable/bus.xml similarity index 100% rename from ui/src/commonMain/composeResources/drawable/bus.xml rename to ui/shared/src/commonMain/composeResources/drawable/bus.xml diff --git a/ui/src/commonMain/composeResources/drawable/bus_background.xml b/ui/shared/src/commonMain/composeResources/drawable/bus_background.xml similarity index 100% rename from ui/src/commonMain/composeResources/drawable/bus_background.xml rename to ui/shared/src/commonMain/composeResources/drawable/bus_background.xml diff --git a/ui/src/commonMain/composeResources/drawable/bus_icon.xml b/ui/shared/src/commonMain/composeResources/drawable/bus_icon.xml similarity index 100% rename from ui/src/commonMain/composeResources/drawable/bus_icon.xml rename to ui/shared/src/commonMain/composeResources/drawable/bus_icon.xml diff --git a/ui/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/ui/shared/src/commonMain/composeResources/drawable/compose-multiplatform.xml similarity index 100% rename from ui/src/commonMain/composeResources/drawable/compose-multiplatform.xml rename to ui/shared/src/commonMain/composeResources/drawable/compose-multiplatform.xml diff --git a/ui/shared/src/commonMain/composeResources/drawable/my_location_24.xml b/ui/shared/src/commonMain/composeResources/drawable/my_location_24.xml new file mode 100644 index 0000000..e69de29 diff --git a/ui/src/commonMain/composeResources/drawable/train.xml b/ui/shared/src/commonMain/composeResources/drawable/train.xml similarity index 100% rename from ui/src/commonMain/composeResources/drawable/train.xml rename to ui/shared/src/commonMain/composeResources/drawable/train.xml diff --git a/ui/src/commonMain/composeResources/drawable/train_background.xml b/ui/shared/src/commonMain/composeResources/drawable/train_background.xml similarity index 100% rename from ui/src/commonMain/composeResources/drawable/train_background.xml rename to ui/shared/src/commonMain/composeResources/drawable/train_background.xml diff --git a/ui/src/commonMain/composeResources/drawable/train_icon.xml b/ui/shared/src/commonMain/composeResources/drawable/train_icon.xml similarity index 100% rename from ui/src/commonMain/composeResources/drawable/train_icon.xml rename to ui/shared/src/commonMain/composeResources/drawable/train_icon.xml diff --git a/ui/src/commonMain/composeResources/drawable/tram.xml b/ui/shared/src/commonMain/composeResources/drawable/tram.xml similarity index 100% rename from ui/src/commonMain/composeResources/drawable/tram.xml rename to ui/shared/src/commonMain/composeResources/drawable/tram.xml diff --git a/ui/src/commonMain/composeResources/drawable/tram_background.xml b/ui/shared/src/commonMain/composeResources/drawable/tram_background.xml similarity index 100% rename from ui/src/commonMain/composeResources/drawable/tram_background.xml rename to ui/shared/src/commonMain/composeResources/drawable/tram_background.xml diff --git a/ui/src/commonMain/composeResources/drawable/tram_icon.xml b/ui/shared/src/commonMain/composeResources/drawable/tram_icon.xml similarity index 100% rename from ui/src/commonMain/composeResources/drawable/tram_icon.xml rename to ui/shared/src/commonMain/composeResources/drawable/tram_icon.xml 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 new file mode 100644 index 0000000..e84d765 --- /dev/null +++ b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt @@ -0,0 +1,52 @@ +package moe.lava.banksia.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.ui.extensions.getUIProperties +import org.jetbrains.compose.resources.painterResource + +@Composable +fun RouteIcon( + modifier: Modifier = Modifier.Companion, + size: Dp = 40.dp, + routeType: RouteType, +) { + val properties = routeType.getUIProperties() + Image( + painter = painterResource(properties.icon), + contentDescription = null, + modifier = modifier + .size(size) + .aspectRatio(1f) + .padding(size * ICON_PADDING / 2) + .drawBehind { + drawCircle(properties.colour, radius = size.toPx() / 2f) + } + ) +} + +const val ICON_PADDING = 0.25f + +@Preview +@Composable +internal fun RouteIconPreview() { + Row { + RouteIcon(routeType = MetroTrain) + RouteIcon(routeType = MetroTram) + RouteIcon(routeType = MetroBus) + } +} + diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt similarity index 51% rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt rename to ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt index c06fd1e..992b910 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/components/RouteIcon.kt +++ b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/extensions/RouteType.kt @@ -1,27 +1,8 @@ -package moe.lava.banksia.ui.components +package moe.lava.banksia.ui.extensions -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import moe.lava.banksia.data.ptv.structures.PtvRouteType import moe.lava.banksia.model.RouteType -import moe.lava.banksia.model.RouteType.Interstate -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.model.RouteType.RegionalBus -import moe.lava.banksia.model.RouteType.RegionalCoach -import moe.lava.banksia.model.RouteType.RegionalTrain -import moe.lava.banksia.model.RouteType.SkyBus import moe.lava.banksia.resources.Res import moe.lava.banksia.resources.bus import moe.lava.banksia.resources.bus_background @@ -33,7 +14,6 @@ import moe.lava.banksia.resources.tram import moe.lava.banksia.resources.tram_background import moe.lava.banksia.resources.tram_icon import org.jetbrains.compose.resources.DrawableResource -import org.jetbrains.compose.resources.painterResource data class RouteTypeProperties( val colour: Color, @@ -49,31 +29,31 @@ const val VLINE_PURPLE = 0xFF8F1A95 fun RouteType.getUIProperties(): RouteTypeProperties { val colour = when (this) { - MetroTrain -> TRAIN_BLUE - MetroTram -> TRAM_GREEN - MetroBus -> BUS_ORANGE - RegionalTrain -> VLINE_PURPLE - RegionalCoach -> VLINE_PURPLE - RegionalBus -> VLINE_PURPLE - SkyBus -> BUS_ORANGE - Interstate -> BUS_ORANGE + RouteType.MetroTrain -> TRAIN_BLUE + RouteType.MetroTram -> TRAM_GREEN + RouteType.MetroBus -> BUS_ORANGE + RouteType.RegionalTrain -> VLINE_PURPLE + RouteType.RegionalCoach -> VLINE_PURPLE + RouteType.RegionalBus -> VLINE_PURPLE + RouteType.SkyBus -> BUS_ORANGE + RouteType.Interstate -> BUS_ORANGE } val (drawable, background, icon) = when (this) { - MetroTrain, - RegionalTrain, - Interstate -> Triple( + RouteType.MetroTrain, + RouteType.RegionalTrain, + RouteType.Interstate -> Triple( Res.drawable.train, Res.drawable.train_background, Res.drawable.train_icon ) - MetroTram -> Triple( + RouteType.MetroTram -> Triple( Res.drawable.tram, Res.drawable.tram_background, Res.drawable.tram_icon ) - MetroBus, - RegionalCoach, - RegionalBus, - SkyBus -> Triple( + RouteType.MetroBus, + RouteType.RegionalCoach, + RouteType.RegionalBus, + RouteType.SkyBus -> Triple( Res.drawable.bus, Res.drawable.bus_background, Res.drawable.bus_icon ) } @@ -102,35 +82,3 @@ fun PtvRouteType.getUIProperties(): RouteTypeProperties { return RouteTypeProperties(colour, drawable, background, icon) } -@Composable -fun RouteIcon( - modifier: Modifier = Modifier.Companion, - size: Dp = 40.dp, - routeType: RouteType, -) { - val properties = routeType.getUIProperties() - Image( - painter = painterResource(properties.icon), - contentDescription = null, - modifier = modifier - .size(size) - .aspectRatio(1f) - .padding(size * ICON_PADDING / 2) - .drawBehind { - drawCircle(properties.colour, radius = size.toPx() / 2f) - } - ) -} - -const val ICON_PADDING = 0.25f - -@Preview -@Composable -private fun RouteIconPreview() { - Row { - RouteIcon(routeType = MetroTrain) - RouteIcon(routeType = MetroTram) - RouteIcon(routeType = MetroBus) - } -} - diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt b/ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt similarity index 100% rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt rename to ui/shared/src/commonMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.kt diff --git a/ui/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt b/ui/shared/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt similarity index 100% rename from ui/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt rename to ui/shared/src/iosMain/kotlin/moe/lava/banksia/ui/platform/BanksiaTheme.ios.kt diff --git a/ui/src/commonMain/composeResources/drawable/my_location_24.xml b/ui/src/commonMain/composeResources/drawable/my_location_24.xml deleted file mode 100644 index 683a624..0000000 --- a/ui/src/commonMain/composeResources/drawable/my_location_24.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/InfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/InfoPanel.kt deleted file mode 100644 index 8d525f3..0000000 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/InfoPanel.kt +++ /dev/null @@ -1,177 +0,0 @@ -package moe.lava.banksia.ui.layout - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -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.WindowInsets -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeContent -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.windowInsetsBottomHeight -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.LoadingIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -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.unit.Dp -import androidx.compose.ui.unit.coerceAtMost -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.delay -import moe.lava.banksia.ui.components.RouteIcon -import moe.lava.banksia.ui.screens.map.MapScreenEvent -import moe.lava.banksia.ui.state.InfoPanelState -import kotlin.time.Duration.Companion.milliseconds - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun InfoPanel( - state: InfoPanelState, - onEvent: (MapScreenEvent) -> Unit, - onPeekHeightChange: (Dp) -> Unit, -) { - if (state is InfoPanelState.None) - return - - val localDensity = LocalDensity.current - var delayedLoad by remember { mutableStateOf(false) } - - LaunchedEffect(state.loading) { - if (state.loading) { - delay(200.milliseconds) - delayedLoad = true - } else { - delayedLoad = false - } - } - - Column( - Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - .onSizeChanged { - onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) }) - } - ) { - Box { - when (state) { - is InfoPanelState.Route -> RouteInfoPanel(state, onEvent) - is InfoPanelState.Stop -> StopInfoPanel(state, onEvent) - is InfoPanelState.Run -> RunInfoPanel(state, onEvent) - is InfoPanelState.None -> throw UnsupportedOperationException() - } - - this@Column.AnimatedVisibility( - modifier = Modifier.align(Alignment.TopEnd), - visible = delayedLoad, - label = "sheet-loading", - enter = fadeIn() + scaleIn(), - exit = fadeOut() + scaleOut(), - ) { - LoadingIndicator( - modifier = Modifier.size(48.dp) - ) - } - } - Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeContent)) - } -} - -@Composable -private inline fun RouteInfoPanel( - state: InfoPanelState.Route, - onEvent: (MapScreenEvent) -> Unit, -) { - Column(Modifier.fillMaxWidth()) { - Row { - RouteIcon(routeType = state.type) - Text( - state.name, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, - textAlign = TextAlign.Start - ) - } - } -} - -@Composable -private inline fun RunInfoPanel( - state: InfoPanelState.Run, - onEvent: (MapScreenEvent) -> Unit, -) { - Column(Modifier.fillMaxWidth()) { - Row { - RouteIcon(routeType = state.type) - Text( - "${state.direction} via ${state.routeName ?: "..."}", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, - textAlign = TextAlign.Start - ) - } - } -} - -@Composable -private inline fun StopInfoPanel( - state: InfoPanelState.Stop, - onEvent: (MapScreenEvent) -> Unit, -) { - 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) { - Text( - name, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold - ) - Text( - formatted, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = 5.dp) - ) - } - } - } - } -} 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 new file mode 100644 index 0000000..55eac69 --- /dev/null +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/InfoPanel.kt @@ -0,0 +1,97 @@ +package moe.lava.banksia.ui.layout.info + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.layout.Box +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.padding +import androidx.compose.foundation.layout.safeContent +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LoadingIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +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 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( + state: InfoPanelState, + onEvent: (InfoPanelEvent) -> Unit, + onPeekHeightChange: (Dp) -> Unit, +) { + if (state is InfoPanelState.None) + return + + val localDensity = LocalDensity.current + var delayedLoad by remember { mutableStateOf(false) } + + LaunchedEffect(state.loading) { + if (state.loading) { + delay(200.milliseconds) + delayedLoad = true + } else { + delayedLoad = false + } + } + + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .onSizeChanged { + onPeekHeightChange(with(localDensity) { it.height.toDp().coerceAtMost(250.dp) }) + } + ) { + Box { + when (state) { + is RouteInfoPanelState -> RouteInfoPanel(state, onEvent) + is StopInfoPanelState -> StopInfoPanel(state, onEvent) + is TripInfoPanelState -> TripInfoPanel(state, onEvent) + is InfoPanelState.None -> throw UnsupportedOperationException() + } + + this@Column.AnimatedVisibility( + modifier = Modifier.align(Alignment.TopEnd), + visible = delayedLoad, + label = "sheet-loading", + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut(), + ) { + LoadingIndicator( + modifier = Modifier.size(48.dp) + ) + } + } + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeContent)) + } +} 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 new file mode 100644 index 0000000..655caca --- /dev/null +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/RouteInfoPanel.kt @@ -0,0 +1,40 @@ +package moe.lava.banksia.ui.layout.info + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.model.RouteType +import moe.lava.banksia.ui.components.RouteIcon + +sealed class RouteInfoPanelEvent : InfoPanelEvent() + +data class RouteInfoPanelState( + val name: String, + val type: RouteType, +) : InfoPanelState() { + override val loading = false +} + +@Composable +internal fun RouteInfoPanel( + state: RouteInfoPanelState, + onEvent: (RouteInfoPanelEvent) -> Unit, +) { + Column(Modifier.fillMaxWidth()) { + Row { + RouteIcon(routeType = state.type) + Text( + state.name, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Start + ) + } + } +} 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 new file mode 100644 index 0000000..dbe3b29 --- /dev/null +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt @@ -0,0 +1,75 @@ +package moe.lava.banksia.ui.layout.info + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.unit.dp + +sealed class StopInfoPanelEvent : InfoPanelEvent() + +data class StopInfoPanelState( + 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) +} + +@Composable +internal fun StopInfoPanel( + state: StopInfoPanelState, + onEvent: (StopInfoPanelEvent) -> Unit, +) { + 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) { + Text( + name, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Text( + formatted, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(horizontal = 5.dp) + ) + } + } + } + } +} 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 new file mode 100644 index 0000000..7b7dcf9 --- /dev/null +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/TripInfoPanel.kt @@ -0,0 +1,41 @@ +package moe.lava.banksia.ui.layout.info + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.model.RouteType +import moe.lava.banksia.ui.components.RouteIcon + +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: TripInfoPanelState, + onEvent: (TripInfoPanelEvent) -> Unit, +) { + Column(Modifier.fillMaxWidth()) { + Row { + RouteIcon(routeType = state.type) + Text( + "${state.direction} via ${state.routeName ?: "..."}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Start + ) + } + } +} 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 15388be..f4319be 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 @@ -35,17 +35,15 @@ import kotlinx.coroutines.launch import moe.lava.banksia.resources.Res import moe.lava.banksia.resources.my_location_24 import moe.lava.banksia.ui.layout.AppBottomSheet -import moe.lava.banksia.ui.layout.InfoPanel 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.platform.BanksiaTheme -import moe.lava.banksia.ui.state.InfoPanelState -import moe.lava.banksia.util.Point import org.jetbrains.compose.resources.painterResource import org.koin.compose.viewmodel.koinViewModel -val MELBOURNE = Point(-37.8136, 144.9631) - @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable fun MapScreen( @@ -78,14 +76,20 @@ fun MapScreen( Scaffold { Maps( modifier = Modifier.fillMaxSize(), - state = mapState, - onEvent = viewModel::handleEvent, - cameraPositionFlow = viewModel.cameraChangeEmitter, - extInsets = WindowInsets(top = with(LocalDensity.current) { + insets = WindowInsets(top = with(LocalDensity.current) { SearchBarDefaults.InputFieldHeight.roundToPx() }, bottom = sheetState.bottomInset), - setLastKnownLocation = viewModel::setLastKnownLocation, + stops = mapState.stops, +// vehicles = mapState.vehicles, + onStopClicked = { stop -> + viewModel.handleEvent(MapScreenEvent.SelectStop(stop)) + }, +// onEvent = viewModel::handleEvent, +// cameraPositionFlow = viewModel.cameraChangeEmitter, +// setLastKnownLocation = viewModel::setLastKnownLocation, ) + +// onEvent() Searcher( state = searchState, onEvent = viewModel::handleEvent, 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 99ac1fa..2e19c68 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 @@ -13,41 +13,45 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.takeWhile 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.data.ptv.PtvService -import moe.lava.banksia.data.ptv.structures.PtvRoute import moe.lava.banksia.model.Route import moe.lava.banksia.model.RouteType -import moe.lava.banksia.ui.components.getUIProperties -import moe.lava.banksia.ui.state.InfoPanelState +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.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.MapState import moe.lava.banksia.ui.state.SearchState -import moe.lava.banksia.ui.utils.map.CameraPosition -import moe.lava.banksia.ui.utils.map.CameraPositionBounds -import moe.lava.banksia.ui.utils.map.Marker -import moe.lava.banksia.ui.utils.map.Polyline 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.Instant +import kotlin.time.Duration.Companion.minutes sealed class MapScreenEvent { data object DismissState : MapScreenEvent() data class SelectRoute(val id: String?) : MapScreenEvent() data class SelectRun(val ref: String?) : MapScreenEvent() - data class SelectStop(val typeIdPair: Pair?) : MapScreenEvent() + data class SelectStop(val id: String?) : MapScreenEvent() data class SearchUpdate(val text: String) : MapScreenEvent() } -data class InternalState( +private data class InternalState( val route: String? = null, - val stop: Pair? = null, + val stop: String? = null, val run: String? = null, ) @@ -55,6 +59,7 @@ class MapScreenViewModel( private val ptvService: PtvService, private val routeRepository: RouteRepository, private val stopRepository: StopRepository, + private val stopTimeRepository: StopTimeRepository, ) : ViewModel() { private var state = InternalState() set(value) { @@ -92,12 +97,18 @@ class MapScreenViewModel( is MapScreenEvent.DismissState -> dismissState() is MapScreenEvent.SelectRoute -> state = InternalState(route = event.id) is MapScreenEvent.SelectRun -> state = state.copy(run = event.ref, stop = null) - is MapScreenEvent.SelectStop -> state = state.copy(stop = event.typeIdPair, run = null) + is MapScreenEvent.SelectStop -> state = state.copy(stop = event.id, run = null) is MapScreenEvent.SearchUpdate -> searchUpdate(event.text) } } } + fun handleEvent(event: InfoPanelEvent) { + viewModelScope.launch { +// when (event) { } + } + } + fun bindTracker(locationTracker: LocationTracker) { locationTrackerJob = locationTracker.getLocationsFlow() .onEach { lastKnownLocation = Point(it.latitude, it.longitude) } @@ -161,7 +172,7 @@ class MapScreenViewModel( val route = routeRepository.get(routeId) // val gtfsRoute = ptvService.route(routeId) iInfoState.update { - InfoPanelState.Route( + RouteInfoPanelState( name = route.name, type = route.type, ) @@ -186,7 +197,7 @@ class MapScreenViewModel( .onEach { run -> if (routeName == null) { iInfoState.update { - InfoPanelState.Run( + TripInfoPanelState( direction = run.destinationName, type = RouteType.MetroTrain, // XXX HACK TODO FIXME ) @@ -195,7 +206,7 @@ class MapScreenViewModel( } iInfoState.update { - InfoPanelState.Run( + TripInfoPanelState( direction = run.destinationName, type = RouteType.MetroTrain, // FIXME HACK XXX TODO routeName = routeName, @@ -206,12 +217,11 @@ class MapScreenViewModel( } // [TODO]: Cleanup - private suspend fun switchStop(pair: Pair?) { - if (pair == null) { + private suspend fun switchStop(id: String?) { + if (id == null) { iInfoState.update { InfoPanelState.None } return } - val (type, id) = pair val stop = stopRepository.get(id) // val stop = ptvService.stop(routeType, stopId) @@ -219,52 +229,40 @@ class MapScreenViewModel( val name = split[0] val subname = split.getOrNull(1) iInfoState.update { - InfoPanelState.Stop( + StopInfoPanelState( id = stop.id, name = name, subname = subname, ) } - val res = ptvService.departures(type, stop.id) - // Map< - // Pair, - // Pair> - // > - val timetable = HashMap, Pair>>() - res.departures.forEach { dep -> - val key = Pair(dep.directionId, dep.routeId) - val direction = ptvService.direction(dep.directionId, dep.routeId) - val route = res.routes[dep.routeId.toString()] - val prefix = route?.let { if (it.routeNumber == "") "" else "${it.routeNumber} - " } ?: "" - val element = timetable.getOrPut(key) { Pair(prefix + direction.directionName, mutableListOf()) }.second - if (element.size >= 5) - return@forEach - - val date = Instant.parse(dep.estimatedDepartureUtc ?: dep.scheduledDepartureUtc) - val min = (date - Clock.System.now()).inWholeMinutes - if (min <= -5) - return@forEach - if (min >= 65) - element.add("${((min + 30.0) / 60.0).toInt()}hr") - else - element.add("${min}mn") - } - val departures = timetable.values.sortedBy { it.first }.map { (name, list) -> - if (list.isEmpty()) - InfoPanelState.Stop.Departure(name, "No departures") - else - InfoPanelState.Stop.Departure(name, list.joinToString(" | ")) - } + 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" + } + } + StopInfoPanelState.Departure(headsign, times) + } iInfoState.update { - if (it !is InfoPanelState.Stop) + if (it !is StopInfoPanelState) it else it.copy(departures = departures) } } - private suspend fun buildPolylines(route: PtvRoute) { + /*private suspend fun buildPolylines(route: PtvRoute) { val routeWithGeo = if (route.geopath.isEmpty()) ptvService.route(route.routeId, true) else @@ -294,9 +292,9 @@ class MapScreenViewModel( iMapState.update { it.copy(polylines = polylines) } newCameraPosition?.let { iCameraChangeEmitter.emit(it.box()) } - } + }*/ - private fun buildRuns(route: PtvRoute) { + /*private fun buildRuns(route: PtvRoute) { ptvService .runsFlow(route.routeId) .waitUntilSubscribed(iInfoState) @@ -317,19 +315,16 @@ class MapScreenViewModel( iMapState.update { it.copy(vehicles = markers) } } .launchIn(viewModelScope) - - } + }*/ private suspend fun buildStops(route: Route) { val stops = stopRepository.getByRoute(route.id) - val colour = route.type.getUIProperties().colour val markers = stops .map { stop -> Marker.Stop( point = stop.pos, id = stop.id, - colour = colour, type = route.type, ) } diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/Maps.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/Maps.kt deleted file mode 100644 index fe20f9f..0000000 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/screens/map/Maps.kt +++ /dev/null @@ -1,210 +0,0 @@ -@file:Suppress("COMPOSE_APPLIER_CALL_MISMATCH") - -package moe.lava.banksia.ui.screens.map - -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.add -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.coroutines.flow.Flow -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromJsonElement -import moe.lava.banksia.model.RouteType -import moe.lava.banksia.ui.components.getUIProperties -import moe.lava.banksia.ui.platform.BanksiaTheme -import moe.lava.banksia.ui.state.MapState -import moe.lava.banksia.ui.utils.map.CameraPosition -import moe.lava.banksia.ui.utils.map.Marker -import moe.lava.banksia.util.BoxedValue -import moe.lava.banksia.util.Point -import moe.lava.banksia.util.log -import org.maplibre.compose.camera.rememberCameraState -import org.maplibre.compose.expressions.dsl.case -import org.maplibre.compose.expressions.dsl.const -import org.maplibre.compose.expressions.dsl.convertToString -import org.maplibre.compose.expressions.dsl.feature -import org.maplibre.compose.expressions.dsl.switch -import org.maplibre.compose.layers.CircleLayer -import org.maplibre.compose.map.MapOptions -import org.maplibre.compose.map.MaplibreMap -import org.maplibre.compose.map.OrnamentOptions -import org.maplibre.compose.sources.GeoJsonData -import org.maplibre.compose.sources.rememberGeoJsonSource -import org.maplibre.compose.style.BaseStyle -import org.maplibre.compose.util.ClickResult -import org.maplibre.spatialk.geojson.BoundingBox -import org.maplibre.spatialk.geojson.FeatureCollection -import org.maplibre.spatialk.geojson.Position -import org.maplibre.spatialk.geojson.dsl.addFeature -import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection -import org.maplibre.compose.camera.CameraPosition as MLCameraPosition -import org.maplibre.spatialk.geojson.Point as MLPoint - -fun Point.toPos(): Position = Position(this.lng, this.lat) - -@Serializable -data class MarkerProps( - val type: RouteType, -) - -private fun buildMarkers(markers: List): FeatureCollection { - return buildFeatureCollection { - markers.forEach { marker -> - val type = when (marker) { - is Marker.Stop -> marker.type - is Marker.Vehicle -> marker.type - } - val id = when (marker) { - is Marker.Stop -> marker.id - is Marker.Vehicle -> marker.ref - } - addFeature( - geometry = MLPoint(marker.point.toPos()), - properties = MarkerProps(type), - ) { - setId(id) - } - } - } -} - -private val colorTypeExpression @Composable get() = switch( - input = feature["type"].convertToString(), - cases = RouteType.entries.map { - case(label = it.name, output = const(it.getUIProperties().colour)) - }.toTypedArray(), - fallback = const(BanksiaTheme.colors.surface), -) - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun Maps( - modifier: Modifier, - state: MapState, - onEvent: (MapScreenEvent) -> Unit, - cameraPositionFlow: Flow>, - setLastKnownLocation: (Point) -> Unit, - extInsets: WindowInsets, -) { - val camPos = rememberCameraState( - MLCameraPosition( - zoom = 16.0, - target = MELBOURNE.toPos() - ) - ) - val newCameraPos by cameraPositionFlow.collectAsStateWithLifecycle(null) - LaunchedEffect(newCameraPos) { - log("maps", "newPos ${newCameraPos?.value}") - val pos = newCameraPos?.value ?: return@LaunchedEffect - if (pos.bounds != null) { - val (northeast, southwest) = pos.bounds - camPos.animateTo( - boundingBox = BoundingBox( - southwest.toPos(), - northeast.toPos() - ) - ) - } else { - camPos.animateTo(MLCameraPosition( - target = pos.centre.toPos(), - zoom = 16.0, - )) - } - } -// -// val ctx = LocalContext.current -// val fusedLocation = remember { LocationServices.getFusedLocationProviderClient(ctx) } -// LaunchedEffect(Unit) { -// @SuppressLint("MissingPermission") -// fusedLocation.lastLocation.addOnSuccessListener { -// if (it != null) { -// camPos.position = MLCameraPosition( -// zoom = 16.0, -// target = Position(it.longitude, it.latitude) -// ) -// setLastKnownLocation(Point(it.latitude, it.longitude)) -// } -// } -// } - - MaplibreMap( - modifier = modifier, - baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/positron"), - cameraState = camPos, - options = MapOptions( - ornamentOptions = OrnamentOptions( - padding = WindowInsets.safeDrawing.add(extInsets).asPaddingValues(), - isScaleBarEnabled = false, - isAttributionEnabled = false, - ) - ) - ) { - if (state.stops.isNotEmpty()) { - val stopsSource = rememberGeoJsonSource( - GeoJsonData.Features(buildMarkers(state.stops)) - ) - CircleLayer( - id = "maps-stops0", - source = stopsSource, - color = const(BanksiaTheme.colors.surface), - radius = const(3.dp), - strokeWidth = const(2.dp), - strokeColor = colorTypeExpression, - ) - CircleLayer( - id = "maps-stops0-clickhandler", - source = stopsSource, - color = const(Color.Transparent), - radius = const(12.dp), - onClick = { features -> - val feature = features[0] - val marker = Json.decodeFromJsonElement(feature.properties!!) - onEvent(MapScreenEvent.SelectStop(marker.type to feature.id!!.content)) - ClickResult.Consume - } - ) - } - - // TODO -// if (state.vehicles.isNotEmpty()) { -// val stopsSource = rememberGeoJsonSource( -// GeoJsonData.Features(buildMarkers(state.vehicles)) -// ) -// SymbolLayer -// CircleLayer( -// id = "maps-vehicles0", -// source = stopsSource, -// color = const(BanksiaTheme.colors.surface), -// radius = const(3.dp), -// strokeWidth = const(2.dp), -// strokeColor = colorTypeExpression, -// onClick = { features -> -// val feature = features[0] -// val marker = Json.decodeFromJsonElement(feature.properties!!) -// onEvent(MapScreenEvent.SelectStop(marker.type to feature.id!!.content)) -// ClickResult.Consume -// } -// ) -// } -// -// if (state.polylines.isNotEmpty()) { -// val polySource = rememberGeoJsonSource( -// -// ) -// LineLayer( -// id = "maps-routeline", -// source = polySource, -// color = colorTypeExpression, -// ) -// } - } -} 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 b0acbec..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 Run( - val direction: String, - val type: RouteType, - val routeName: String? = null, - ) : InfoPanelState() { - override val loading = routeName == null - } - - 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) - } -} diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt index ff71bf4..82ba204 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/MapState.kt @@ -1,7 +1,7 @@ package moe.lava.banksia.ui.state -import moe.lava.banksia.ui.utils.map.Marker -import moe.lava.banksia.ui.utils.map.Polyline +import moe.lava.banksia.ui.map.util.Marker +import moe.lava.banksia.ui.map.util.Polyline data class MapState( val stops: List = listOf(), diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Marker.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Marker.kt deleted file mode 100644 index 2efe33d..0000000 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/utils/map/Marker.kt +++ /dev/null @@ -1,22 +0,0 @@ -package moe.lava.banksia.ui.utils.map - -import androidx.compose.ui.graphics.Color -import moe.lava.banksia.model.RouteType -import moe.lava.banksia.util.Point - -sealed class Marker { - abstract val point: Point - - data class Stop( - override val point: Point, - val id: String, - val type: RouteType, - val colour: Color, - ) : Marker() - - data class Vehicle( - override val point: Point, - val ref: String, - val type: RouteType, - ) : Marker() -}