From 6770c016133b1b290d8d903287baa1c90ff72ed9 Mon Sep 17 00:00:00 2001 From: LavaDesu Date: Fri, 8 Aug 2025 01:59:32 +1000 Subject: [PATCH] feat: di, db, and preliminary server-side gtfs parsing --- gradle/libs.versions.toml | 19 ++- server/build.gradle.kts | 9 ++ .../moe/lava/banksia/server/Application.kt | 27 +++- .../lava/banksia/server/di/ServerModules.kt | 11 ++ .../lava/banksia/server/gtfs/GtfsHandler.kt | 122 ++++++++++++++++++ .../server/gtfs/structures/GtfsRoute.kt | 15 +++ .../server/gtfs/structures/GtfsShape.kt | 13 ++ shared/build.gradle.kts | 14 +- .../moe.lava.banksia.room.Database/1.json | 72 +++++++++++ .../lava/banksia/di/PlatformModule.android.kt | 22 ++++ .../banksia/data/ptv/structures/PtvRoute.kt | 22 +--- .../moe/lava/banksia/di/CommonModules.kt | 12 ++ .../moe/lava/banksia/di/PlatformModule.kt | 17 +++ .../kotlin/moe/lava/banksia/model/Route.kt | 12 ++ .../moe/lava/banksia/model/RouteType.kt | 23 ++++ .../kotlin/moe/lava/banksia/model/Shape.kt | 16 +++ .../kotlin/moe/lava/banksia/room/Database.kt | 28 ++++ .../banksia/room/converter/ShapeConverter.kt | 43 ++++++ .../moe/lava/banksia/room/dao/RouteDao.kt | 25 ++++ .../moe/lava/banksia/room/dao/ShapeDao.kt | 22 ++++ .../moe/lava/banksia/di/PlatformModule.ios.kt | 15 +++ .../moe/lava/banksia/di/PlatformModule.jvm.kt | 20 +++ 22 files changed, 555 insertions(+), 24 deletions(-) create mode 100644 server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt create mode 100644 server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt create mode 100644 server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt create mode 100644 server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt create mode 100644 shared/schemas/moe.lava.banksia.room.Database/1.json create mode 100644 shared/src/androidMain/kotlin/moe/lava/banksia/di/PlatformModule.android.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/di/PlatformModule.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/ShapeConverter.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt create mode 100644 shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt create mode 100644 shared/src/iosMain/kotlin/moe/lava/banksia/di/PlatformModule.ios.kt create mode 100644 shared/src/jvmMain/kotlin/moe/lava/banksia/di/PlatformModule.jvm.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa17427..cd7c5bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,15 +15,20 @@ compose-multiplatform = "1.8.0-beta02" coroutines = "1.9.0" geo = "0.8.0" junit = "4.13.2" -kotlin = "2.1.10" +koin = "4.1.0" +kotlin = "2.2.0" kotlinxDatetime = "0.6.2" +kotlinxSerializationCsv = "0.2.18" kotlinxSerializationJson = "1.8.1" +ksp = "2.2.0-2.0.2" ktor = "3.1.1" logback = "1.5.17" mapsCompose = "6.4.1" okio = "3.11.0" playServicesLocation = "21.3.0" playServicesMaps = "19.1.0" +sqlite = "2.5.2" +room = "2.7.2" secretsGradlePlugin = "2.0.1" [libraries] @@ -34,9 +39,12 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } +kotlinx-serialization-csv = { module = "com.lightningkite:kotlinx-serialization-csv-durable", version.ref = "kotlinxSerializationCsv" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } @@ -51,6 +59,11 @@ maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = okio = { module = "com.squareup.okio:okio", version.ref = "okio" } play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" } play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" } +room-common = { group = "androidx.room", name = "room-common", version.ref = "room" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +sqlite-bundled = { group = "androidx.sqlite", name = "sqlite-bundled", version.ref = "sqlite" } secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "compose-multiplatform" } @@ -60,7 +73,9 @@ androidLibrary = { id = "com.android.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" } -ktor = { id = "io.ktor.plugin", version.ref = "ktor" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +ktor = { id = "io.ktor.plugin", version.ref = "ktor" } +room = { id = "androidx.room", version.ref = "room" } secretsGradle = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" } diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 01f96da..8ecec3b 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.kotlinJvm) + alias(libs.plugins.kotlinSerialization) alias(libs.plugins.ktor) application } @@ -14,8 +15,16 @@ application { dependencies { implementation(projects.shared) implementation(libs.logback) + implementation(libs.koin.core) + implementation(libs.koin.ktor) + implementation(libs.kotlinx.serialization.csv) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.contentnegotiation) + implementation(libs.ktor.client.okhttp) implementation(libs.ktor.server.core) implementation(libs.ktor.server.netty) + implementation(libs.room.runtime) + implementation(libs.sqlite.bundled) testImplementation(libs.ktor.server.tests) testImplementation(libs.kotlin.test.junit) } diff --git a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt index 6ef6271..1969312 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt +++ b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt @@ -1,11 +1,22 @@ package moe.lava.banksia.server +import io.ktor.client.HttpClient import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.application.log import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import io.ktor.server.response.respondText import io.ktor.server.routing.get import io.ktor.server.routing.routing +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import moe.lava.banksia.di.CommonModules +import moe.lava.banksia.server.di.ServerModules +import moe.lava.banksia.server.gtfs.GtfsHandler +import org.koin.dsl.module +import org.koin.ktor.ext.inject +import org.koin.ktor.plugin.Koin fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module) @@ -13,9 +24,21 @@ fun main() { } fun Application.module() { + install(Koin) { + modules(module { single { log } }) + modules(CommonModules, ServerModules) + } + + val client = HttpClient() + routing { - get("/") { - call.respondText("Ktor: Hi") + get("/update") { + val datasetUrl = call.parameters["url"] ?: "https://opendata.transport.vic.gov.au/dataset/3f4e292e-7f8a-4ffe-831f-1953be0fe448/resource/e4966d78-dc64-4a1d-a751-2470c9eaf034/download/gtfs.zip" + call.respondText("received") + launch(context = Dispatchers.IO) { + val handler by inject() + handler.update(datasetUrl) + } } } } diff --git a/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt b/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt new file mode 100644 index 0000000..5938cee --- /dev/null +++ b/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt @@ -0,0 +1,11 @@ +package moe.lava.banksia.server.di + +import io.ktor.client.HttpClient +import moe.lava.banksia.server.gtfs.GtfsHandler +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val ServerModules = module { + single { HttpClient() } + singleOf(::GtfsHandler) +} 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 new file mode 100644 index 0000000..7c537f4 --- /dev/null +++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsHandler.kt @@ -0,0 +1,122 @@ +package moe.lava.banksia.server.gtfs + +import com.lightningkite.kotlinx.serialization.csv.CsvFormat +import com.lightningkite.kotlinx.serialization.csv.StringDeferringConfig +import io.ktor.client.HttpClient +import io.ktor.client.request.prepareRequest +import io.ktor.client.request.url +import io.ktor.client.statement.bodyAsChannel +import io.ktor.util.cio.writeChannel +import io.ktor.util.logging.Logger +import io.ktor.utils.io.copyAndClose +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.modules.EmptySerializersModule +import moe.lava.banksia.model.Route +import moe.lava.banksia.model.RouteType +import moe.lava.banksia.model.Shape +import moe.lava.banksia.room.dao.RouteDao +import moe.lava.banksia.room.dao.ShapeDao +import moe.lava.banksia.server.gtfs.structures.GtfsRoute +import moe.lava.banksia.server.gtfs.structures.GtfsShape +import moe.lava.banksia.util.Point +import java.io.File +import java.util.zip.ZipFile + +class GtfsHandler( + private val log: Logger, + private val client: HttpClient, + + private val routeDao: RouteDao, + private val shapeDao: ShapeDao, +) { + private val csv = CsvFormat(StringDeferringConfig(EmptySerializersModule())) + private val datasetPath = File("/tmp/banksia", "dataset.zip") + + suspend fun update(datasetUrl: String) { + val parentDir = datasetPath.parentFile + if (parentDir.exists() && !log.isTraceEnabled) // XXX: hacky check for dev env + parentDir.deleteRecursively() + + parentDir.mkdirs() + + log.info("fetching..") + client.prepareRequest { + url(datasetUrl) + }.execute { resp -> + if (!datasetPath.exists()) + resp.bodyAsChannel().copyAndClose(datasetPath.writeChannel()) + log.info("fetched!") + } + + log.info("extracting...") + val files = extractAll(datasetPath) + + log.info("parsing routes...") + val routes = files + .filter { it.name == "routes.txt" } + .flatMap { fd -> parseRoutes(fd) } + + log.info("inserting routes...") + routeDao.deleteAll() + routeDao.insertAll(*routes.toTypedArray()) + + log.info("parsing shapes...") + val shapes = files + .filter { it.name == "shapes.txt" } + .flatMap { fd -> parseShapes(fd) } + + log.info("inserting shapes...") + shapeDao.deleteAll() + shapeDao.insertAll(*shapes.toTypedArray()) + + log.info("done!") + } + + private fun parseRoutes(fd: File) = + fd.parseCsv() + .map { with(it) { + Route( + id = route_id, + type = RouteType.from(fd.parentFile.name.toInt()), + number = route_short_name, + name = route_long_name, + ) + } } + + private fun parseShapes(fd: File) = + fd.parseCsv() + .groupBy { it.shape_id } + .map { (id, group) -> + val points = group + .sortedBy { it.shape_pt_sequence } + .map { Point(it.shape_pt_lat, it.shape_pt_lon) } + + Shape(id, points) + } + + + private fun extract(fd: File): List { + val outputs = mutableListOf() + ZipFile(fd).use { zip -> + for (entry in zip.entries()) { + zip.getInputStream(entry).use { input -> + val out = File(fd.parentFile, entry.name) + out.parentFile.mkdirs() + out.outputStream().use { output -> + input.copyTo(output) + } + outputs.add(out) + } + } + } + return outputs + } + + private fun extractAll(fd: File) = extract(fd).flatMap(::extract) + + private fun File.parseCsv(): List = this + .readText() + .replace("\uFEFF", "") // remove bom + .replace("\r\n", "\n") // crlf -> lf + .let { csv.decodeFromString(it) } +} diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt new file mode 100644 index 0000000..c4eabeb --- /dev/null +++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsRoute.kt @@ -0,0 +1,15 @@ +package moe.lava.banksia.server.gtfs.structures + +import kotlinx.serialization.Serializable + +@Suppress("PropertyName") +@Serializable +data class GtfsRoute( + val route_id: String, + val agency_id: String, + val route_short_name: String, + val route_long_name: String, + val route_type: String, + val route_color: String, + val route_text_color: String, +) diff --git a/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt new file mode 100644 index 0000000..19cdfb5 --- /dev/null +++ b/server/src/main/kotlin/moe/lava/banksia/server/gtfs/structures/GtfsShape.kt @@ -0,0 +1,13 @@ +package moe.lava.banksia.server.gtfs.structures + +import kotlinx.serialization.Serializable + +@Suppress("PropertyName") +@Serializable +data class GtfsShape( + val shape_id: String, + val shape_pt_lat: Double, + val shape_pt_lon: Double, + val shape_pt_sequence: Int, + val shape_dist_traveled: String, +) diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 8f14064..1f05df2 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -5,6 +5,16 @@ plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinSerialization) alias(libs.plugins.androidLibrary) + alias(libs.plugins.ksp) + alias(libs.plugins.room) +} + +room { + schemaDirectory("$projectDir/schemas") +} + +dependencies { + ksp(libs.room.compiler) } kotlin { @@ -27,13 +37,15 @@ kotlin { } commonMain.dependencies { implementation(libs.okio) - // put your Multiplatform dependencies here + implementation(libs.koin.core) implementation(libs.ktor.client.core) implementation(libs.ktor.client.contentnegotiation) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.serialization.json) + implementation(libs.room.runtime) + implementation(libs.sqlite.bundled) } iosMain.dependencies { implementation(libs.ktor.client.darwin) diff --git a/shared/schemas/moe.lava.banksia.room.Database/1.json b/shared/schemas/moe.lava.banksia.room.Database/1.json new file mode 100644 index 0000000..037062e --- /dev/null +++ b/shared/schemas/moe.lava.banksia.room.Database/1.json @@ -0,0 +1,72 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "e536f5a9b1408377bcc449195169648c", + "entities": [ + { + "tableName": "Route", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `number` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "TEXT" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Shape", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` BLOB NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e536f5a9b1408377bcc449195169648c')" + ] + } +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/moe/lava/banksia/di/PlatformModule.android.kt b/shared/src/androidMain/kotlin/moe/lava/banksia/di/PlatformModule.android.kt new file mode 100644 index 0000000..6a198c9 --- /dev/null +++ b/shared/src/androidMain/kotlin/moe/lava/banksia/di/PlatformModule.android.kt @@ -0,0 +1,22 @@ +package moe.lava.banksia.di + +import android.content.Context +import androidx.room.Room +import androidx.room.RoomDatabase +import moe.lava.banksia.room.Database +import org.koin.core.parameter.ParametersHolder +import org.koin.core.scope.Scope + +class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder { + override fun getBuilder(): RoomDatabase.Builder { + val appContext = ctx.applicationContext + val dbFile = appContext.getDatabasePath("room.db") + return Room.databaseBuilder( + context = appContext, + name = dbFile.absolutePath + ) + } +} + +actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder = + AndroidDatabaseBuilder(p.get()) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt index 078d578..3178328 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/data/ptv/structures/PtvRoute.kt @@ -2,18 +2,7 @@ package moe.lava.banksia.data.ptv.structures import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable - -// Ordinals used for sorting in searcher -enum class GtfsSubType(val value: Int) { - MetroTrain(2), - MetroTram(3), - MetroBus(4), - RegionalTrain(1), - RegionalCoach(5), - RegionalBus(6), - SkyBus(11), - Interstate(10), -} +import moe.lava.banksia.model.RouteType @Serializable data class PtvRoute( @@ -24,13 +13,8 @@ data class PtvRoute( @SerialName("route_gtfs_id") val routeGtfsId: String, @SerialName("geopath") val geopath: List, ) { - fun gtfsSubType(): GtfsSubType? { - GtfsSubType.entries.forEach { - if (routeGtfsId.startsWith(it.value.toString())) - return it - } - return null - } + fun gtfsSubType(): RouteType = + RouteType.entries.first { routeGtfsId.startsWith(it.value.toString() + "-") } fun getShortFullName(): String { var res = "" diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt new file mode 100644 index 0000000..850907c --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/di/CommonModules.kt @@ -0,0 +1,12 @@ +package moe.lava.banksia.di + +import moe.lava.banksia.room.Database +import org.koin.dsl.module + +val CommonModules = module { + includes(PlatformModule) + + single { Database.build(get().getBuilder()) } + single { get().getRouteDao() } + single { get().getShapeDao() } +} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/di/PlatformModule.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/di/PlatformModule.kt new file mode 100644 index 0000000..f620bdf --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/di/PlatformModule.kt @@ -0,0 +1,17 @@ +package moe.lava.banksia.di + +import androidx.room.RoomDatabase +import moe.lava.banksia.room.Database +import org.koin.core.parameter.ParametersHolder +import org.koin.core.scope.Scope +import org.koin.dsl.module + +interface PlatformDatabaseBuilder { + fun getBuilder(): RoomDatabase.Builder +} + +expect fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder + +internal val PlatformModule = module { + single { provideDatabaseBuilder(it) } +} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt new file mode 100644 index 0000000..a56e49b --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt @@ -0,0 +1,12 @@ +package moe.lava.banksia.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class Route( + @PrimaryKey val id: String, + val type: RouteType, + val number: String?, + val name: String, +) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt new file mode 100644 index 0000000..d6b4446 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/RouteType.kt @@ -0,0 +1,23 @@ +package moe.lava.banksia.model + +import androidx.room.TypeConverter + +enum class RouteType(val value: Int) { + MetroTrain(2), + MetroTram(3), + MetroBus(4), + RegionalTrain(1), + RegionalCoach(5), + RegionalBus(6), + SkyBus(11), + Interstate(10), + ; + + companion object { + @TypeConverter + fun from(value: Int) = RouteType.entries.first { it.value == value } + + @TypeConverter + fun to(routeType: RouteType) = routeType.value + } +} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt new file mode 100644 index 0000000..c412170 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt @@ -0,0 +1,16 @@ +package moe.lava.banksia.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import moe.lava.banksia.room.converter.ShapeConverter +import moe.lava.banksia.util.Point + +typealias ShapePath = List + +@Entity +@TypeConverters(ShapeConverter::class) +data class Shape( + @PrimaryKey val id: String, + val path: ShapePath, +) diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt new file mode 100644 index 0000000..5c9b009 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/Database.kt @@ -0,0 +1,28 @@ +package moe.lava.banksia.room + +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.sqlite.driver.bundled.BundledSQLiteDriver +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import moe.lava.banksia.model.Route +import moe.lava.banksia.model.RouteType +import moe.lava.banksia.model.Shape +import moe.lava.banksia.room.dao.RouteDao +import moe.lava.banksia.room.dao.ShapeDao +import androidx.room.Database as DatabaseAnnotation + +@DatabaseAnnotation(entities = [Route::class, Shape::class], version = 1) +@TypeConverters(RouteType.Companion::class) +abstract class Database : RoomDatabase() { + abstract fun getRouteDao(): RouteDao + abstract fun getShapeDao(): ShapeDao + + companion object { + fun build(base: Builder) = + base.fallbackToDestructiveMigrationOnDowngrade(true) + .setDriver(BundledSQLiteDriver()) + .setQueryCoroutineContext(Dispatchers.IO) + .build() + } +} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/ShapeConverter.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/ShapeConverter.kt new file mode 100644 index 0000000..bd52b3d --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/converter/ShapeConverter.kt @@ -0,0 +1,43 @@ +package moe.lava.banksia.room.converter + +import androidx.room.TypeConverter +import moe.lava.banksia.model.ShapePath +import moe.lava.banksia.util.Point + +object ShapeConverter { + @TypeConverter + fun from(value: ByteArray): ShapePath { + return value + .asIterable() + .chunked(8) { + (it[0].toLong() and 0xFF) or + (it[1].toLong() and 0xFF shl 8) or + (it[2].toLong() and 0xFF shl 16) or + (it[3].toLong() and 0xFF shl 24) or + (it[4].toLong() and 0xFF shl 32) or + (it[5].toLong() and 0xFF shl 40) or + (it[6].toLong() and 0xFF shl 48) or + (it[7].toLong() and 0xFF shl 56) + } + .map { Double.fromBits(it) } + .chunked(2) + .map { (lat, lng) -> Point(lat, lng) } + } + + @TypeConverter + fun to(path: ShapePath): ByteArray { + return path + .flatMap { (lat, lng) -> listOf(lat.toBits(), lng.toBits()) } + .flatMap { i -> listOf( + i.toByte(), + (i shr 8).toByte(), + (i shr 16).toByte(), + (i shr 24).toByte(), + (i shr 32).toByte(), + (i shr 40).toByte(), + (i shr 48).toByte(), + (i shr 56).toByte(), + ) } + .toByteArray() + } +} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt new file mode 100644 index 0000000..4355068 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/RouteDao.kt @@ -0,0 +1,25 @@ +package moe.lava.banksia.room.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import moe.lava.banksia.model.Route + +@Dao +interface RouteDao { + @Query("SELECT * FROM Route") + suspend fun getAll(): List + + @Query("SELECT * FROM Route WHERE id == :id") + suspend fun get(id: String): Route? + + @Insert + suspend fun insertAll(vararg routes: Route) + + @Delete + suspend fun delete(route: Route) + + @Query("DELETE FROM Route") + suspend fun deleteAll() +} diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt new file mode 100644 index 0000000..56c6114 --- /dev/null +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/room/dao/ShapeDao.kt @@ -0,0 +1,22 @@ +package moe.lava.banksia.room.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import moe.lava.banksia.model.Shape + +@Dao +interface ShapeDao { + @Query("SELECT * FROM Shape WHERE id == :id") + suspend fun get(id: String): Shape? + + @Insert + suspend fun insertAll(vararg shapes: Shape) + + @Delete + suspend fun delete(shape: Shape) + + @Query("DELETE FROM Shape") + suspend fun deleteAll() +} diff --git a/shared/src/iosMain/kotlin/moe/lava/banksia/di/PlatformModule.ios.kt b/shared/src/iosMain/kotlin/moe/lava/banksia/di/PlatformModule.ios.kt new file mode 100644 index 0000000..7e49dbf --- /dev/null +++ b/shared/src/iosMain/kotlin/moe/lava/banksia/di/PlatformModule.ios.kt @@ -0,0 +1,15 @@ +package moe.lava.banksia.di + +import androidx.room.RoomDatabase +import moe.lava.banksia.room.Database +import org.koin.core.parameter.ParametersHolder +import org.koin.core.scope.Scope + +class IosDatabaseBuilder() : PlatformDatabaseBuilder { + override fun getBuilder(): RoomDatabase.Builder { + TODO("Not yet implemented") + } +} + +actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder = + IosDatabaseBuilder() diff --git a/shared/src/jvmMain/kotlin/moe/lava/banksia/di/PlatformModule.jvm.kt b/shared/src/jvmMain/kotlin/moe/lava/banksia/di/PlatformModule.jvm.kt new file mode 100644 index 0000000..84bf64e --- /dev/null +++ b/shared/src/jvmMain/kotlin/moe/lava/banksia/di/PlatformModule.jvm.kt @@ -0,0 +1,20 @@ +package moe.lava.banksia.di + +import androidx.room.Room +import androidx.room.RoomDatabase +import moe.lava.banksia.room.Database +import org.koin.core.parameter.ParametersHolder +import org.koin.core.scope.Scope +import java.io.File + +class JvmDatabaseBuilder() : PlatformDatabaseBuilder { + override fun getBuilder(): RoomDatabase.Builder { + val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db") + return Room.databaseBuilder( + name = dbFile.absolutePath, + ) + } +} + +actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder = + JvmDatabaseBuilder()