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..70676f5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.13.1" +agp = "9.1.0" android-compileSdk = "36" android-minSdk = "24" android-targetSdk = "36" @@ -85,7 +85,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/src/main/kotlin/moe/lava/banksia/server/Application.kt b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt index 4ae3398..76ee8ba 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,24 @@ 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.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,8 +48,11 @@ 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() } + } routing { get("/update") { @@ -137,6 +147,24 @@ fun Application.module() { // .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/gtfs/GtfsHandler.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt index d85d5df..28d50af 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt +++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt @@ -9,11 +9,14 @@ import io.ktor.client.statement.bodyAsChannel import io.ktor.util.cio.writeChannel import io.ktor.util.logging.Logger import io.ktor.utils.io.copyAndClose +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate import kotlinx.serialization.decodeFromString import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.serializer import moe.lava.banksia.Constants import moe.lava.banksia.model.Route +import moe.lava.banksia.model.Service import moe.lava.banksia.model.Shape import moe.lava.banksia.model.Stop import moe.lava.banksia.model.StopTime @@ -22,6 +25,7 @@ import moe.lava.banksia.room.Database import moe.lava.banksia.room.converter.RouteTypeConverter import moe.lava.banksia.room.entity.asEntity import moe.lava.banksia.server.gtfs.structures.GtfsRoute +import moe.lava.banksia.server.gtfs.structures.GtfsService import moe.lava.banksia.server.gtfs.structures.GtfsShape import moe.lava.banksia.server.gtfs.structures.GtfsStop import moe.lava.banksia.server.gtfs.structures.GtfsStopTime @@ -65,6 +69,7 @@ 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) } @@ -72,8 +77,9 @@ class GtfsHandler( addRoutes(files) addStops(files) addShapes(files) - addTrips(files) - addStopTimes(files) + val services = addServices(files) + val trips = addTrips(files, services.associateBy { it.id }) + addStopTimes(files, trips.associateBy { it.id }) updateMetadata(date ?: Clock.System.now().epochSeconds) @@ -174,7 +180,7 @@ class GtfsHandler( ) } } - private suspend fun addStopTimes(files: List) { + private suspend fun addStopTimes(files: List, trips: Map) { val dao = db.stopTimeDao dao.deleteAll() log.info("parsing stop times...") @@ -182,7 +188,7 @@ class GtfsHandler( .filter { it.name == "stop_times.txt" } .forEach { fd -> log.info("parsing stop times for ${fd.parent}...") - parseStopTimes(fd) { seq -> + parseStopTimes(fd, trips) { seq -> seq.chunked(1000000) .forEach { queue -> log.info("converting stop times (${queue.size}) for ${fd.parent}...") @@ -194,7 +200,7 @@ class GtfsHandler( } } - 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 +209,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 +217,61 @@ class GtfsHandler( .let { block(it) } } - private suspend fun addTrips(files: List) { + private suspend fun addServices(files: List): List { + val dao = db.serviceDao + log.info("parsing services...") + val services = files + .filter { it.name == "calendar.txt" } + .flatMap { fd -> parseServices(fd) } + + log.info("inserting services...") + dao.deleteAll() + dao.insertOrReplaceAll(*services.map { it.asEntity() }.toTypedArray()) + + return services + } + + private fun parseServices(fd: File) = + fd.parseCsv() + .map { with(it) { + val days = buildList { + if (monday == 1) add(DayOfWeek.MONDAY) + if (tuesday == 1) add(DayOfWeek.TUESDAY) + if (wednesday == 1) add(DayOfWeek.WEDNESDAY) + if (thursday == 1) add(DayOfWeek.THURSDAY) + if (friday == 1) add(DayOfWeek.FRIDAY) + if (saturday == 1) add(DayOfWeek.SATURDAY) + if (sunday == 1) add(DayOfWeek.SUNDAY) + } + Service( + id = service_id, + days = days, + start = LocalDate.parse(start_date, LocalDate.Formats.ISO_BASIC), + end = LocalDate.parse(end_date, LocalDate.Formats.ISO_BASIC), + ) + } } + + private suspend fun addTrips(files: List, services: Map): List { val dao = db.tripDao log.info("parsing trips...") val trips = files .filter { it.name == "trips.txt" } - .flatMap { fd -> parseTrips(fd) } + .flatMap { fd -> parseTrips(fd, services) } log.info("inserting trips...") dao.deleteAll() dao.insertOrReplaceAll(*trips.map { it.asEntity() }.toTypedArray()) + + return trips } - private fun parseTrips(fd: File) = + 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/GtfsService.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsService.kt new file mode 100644 index 0000000..9347b5e --- /dev/null +++ b/server/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 +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/settings.gradle.kts b/settings.gradle.kts index a33c5ec..4688423 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,10 @@ dependencyResolutionManagement { } } +include(":androidApp") include(":client") include(":server") include(":shared") include(":ui") +include(":ui:maps") +include(":ui:shared") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 1f26a53..953d790 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,10 +1,9 @@ -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) @@ -15,8 +14,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 +27,6 @@ kotlin { freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") } - iosX64() iosArm64() iosSimulatorArm64() @@ -58,27 +58,14 @@ 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/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..8658342 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,7 @@ val CommonModules = module { single { Database.build(get().getBuilder()) } single { get().versionMetadataDao } single { get().routeDao } + single { get().serviceDao } 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/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..89bc24a 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt @@ -7,13 +7,15 @@ import androidx.sqlite.driver.bundled.BundledSQLiteDriver import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import moe.lava.banksia.room.converter.RouteTypeConverter -import moe.lava.banksia.room.dao.VersionMetadataDao import moe.lava.banksia.room.dao.RouteDao +import moe.lava.banksia.room.dao.ServiceDao import moe.lava.banksia.room.dao.ShapeDao import moe.lava.banksia.room.dao.StopDao import moe.lava.banksia.room.dao.StopTimeDao import moe.lava.banksia.room.dao.TripDao +import moe.lava.banksia.room.dao.VersionMetadataDao import moe.lava.banksia.room.entity.RouteEntity +import moe.lava.banksia.room.entity.ServiceEntity import moe.lava.banksia.room.entity.ShapeEntity import moe.lava.banksia.room.entity.StopEntity import moe.lava.banksia.room.entity.StopTimeEntity @@ -22,9 +24,10 @@ import moe.lava.banksia.room.entity.VersionMetadataEntity import androidx.room.Database as DatabaseAnnotation @DatabaseAnnotation( - version = 3, + version = 9, entities = [ RouteEntity::class, + ServiceEntity::class, ShapeEntity::class, StopEntity::class, StopTimeEntity::class, @@ -40,6 +43,7 @@ import androidx.room.Database as DatabaseAnnotation abstract class Database : RoomDatabase() { abstract val versionMetadataDao: VersionMetadataDao abstract val routeDao: RouteDao + abstract val serviceDao: ServiceDao abstract val shapeDao: ShapeDao abstract val stopDao: StopDao abstract val stopTimeDao: StopTimeDao 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/StopTimeDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/StopTimeDao.kt index 88485f4..d5e1744 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,22 @@ 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 * FROM StopTime + INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end` + INNER JOIN Trip ON Trip.serviceId == Service.id + WHERE StopTime.tripId == Trip.id + AND StopTime.stopId == :stopId + """) + suspend fun getForStopDated(stopId: String, days: Int, date: Int): List @Insert suspend fun insertAll(vararg stopTimes: StopTimeEntity) 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/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..3753d44 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")], ) 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/info/InfoPanel.kt similarity index 52% rename from ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/InfoPanel.kt rename to ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/InfoPanel.kt index 8d525f3..fa0354d 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/InfoPanel.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/InfoPanel.kt @@ -1,4 +1,4 @@ -package moe.lava.banksia.ui.layout +package moe.lava.banksia.ui.layout.info import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -7,19 +7,15 @@ 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 @@ -28,17 +24,12 @@ 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 @@ -77,7 +68,7 @@ fun InfoPanel( when (state) { is InfoPanelState.Route -> RouteInfoPanel(state, onEvent) is InfoPanelState.Stop -> StopInfoPanel(state, onEvent) - is InfoPanelState.Run -> RunInfoPanel(state, onEvent) + is InfoPanelState.Trip -> TripInfoPanel(state, onEvent) is InfoPanelState.None -> throw UnsupportedOperationException() } @@ -96,82 +87,3 @@ fun InfoPanel( 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/RouteInfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/RouteInfoPanel.kt new file mode 100644 index 0000000..b55b7c1 --- /dev/null +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/RouteInfoPanel.kt @@ -0,0 +1,32 @@ +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.ui.components.RouteIcon +import moe.lava.banksia.ui.screens.map.MapScreenEvent +import moe.lava.banksia.ui.state.InfoPanelState + +@Composable +internal 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 + ) + } + } +} 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..731ef88 --- /dev/null +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/StopInfoPanel.kt @@ -0,0 +1,63 @@ +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 +import moe.lava.banksia.ui.screens.map.MapScreenEvent +import moe.lava.banksia.ui.state.InfoPanelState + +@Composable +internal 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/TripInfoPanel.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/TripInfoPanel.kt new file mode 100644 index 0000000..2d221b2 --- /dev/null +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/layout/info/TripInfoPanel.kt @@ -0,0 +1,32 @@ +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.ui.components.RouteIcon +import moe.lava.banksia.ui.screens.map.MapScreenEvent +import moe.lava.banksia.ui.state.InfoPanelState + +@Composable +internal fun TripInfoPanel( + state: InfoPanelState.Trip, + 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 + ) + } + } +} 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..70b8ed4 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.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..a3435af 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,41 @@ 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.map.util.CameraPosition +import moe.lava.banksia.ui.map.util.CameraPositionBounds +import moe.lava.banksia.ui.map.util.Marker import moe.lava.banksia.ui.state.InfoPanelState import moe.lava.banksia.ui.state.MapState import moe.lava.banksia.ui.state.SearchState -import moe.lava.banksia.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( val route: String? = null, - val stop: Pair? = null, + val stop: String? = null, val run: String? = null, ) @@ -55,6 +55,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,7 +93,7 @@ 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) } } @@ -186,7 +187,7 @@ class MapScreenViewModel( .onEach { run -> if (routeName == null) { iInfoState.update { - InfoPanelState.Run( + InfoPanelState.Trip( direction = run.destinationName, type = RouteType.MetroTrain, // XXX HACK TODO FIXME ) @@ -195,7 +196,7 @@ class MapScreenViewModel( } iInfoState.update { - InfoPanelState.Run( + InfoPanelState.Trip( direction = run.destinationName, type = RouteType.MetroTrain, // FIXME HACK XXX TODO routeName = routeName, @@ -206,12 +207,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) @@ -226,36 +226,24 @@ class MapScreenViewModel( ) } - 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" + } + } + InfoPanelState.Stop.Departure(headsign, times) + } iInfoState.update { if (it !is InfoPanelState.Stop) it @@ -264,7 +252,7 @@ class MapScreenViewModel( } } - 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 +282,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 +305,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 index b0acbec..5b73914 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/InfoPanelState.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/InfoPanelState.kt @@ -16,14 +16,6 @@ sealed class 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, @@ -35,4 +27,12 @@ sealed class InfoPanelState { data class Departure(val directionName: String, val formattedTimes: String) } + + data class Trip( + val direction: String, + val type: RouteType, + val routeName: String? = null, + ) : InfoPanelState() { + override val loading = routeName == null + } } diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/state/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() -}