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()
-}