feat: di, db, and preliminary server-side gtfs parsing

This commit is contained in:
LavaDesu 2025-08-08 01:59:32 +10:00
parent ccc748dc1f
commit 6770c01613
Signed by: cilly
GPG key ID: 6500251E087653C9
22 changed files with 555 additions and 24 deletions

View file

@ -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)

View file

@ -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')"
]
}
}

View file

@ -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<Database> {
val appContext = ctx.applicationContext
val dbFile = appContext.getDatabasePath("room.db")
return Room.databaseBuilder<Database>(
context = appContext,
name = dbFile.absolutePath
)
}
}
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
AndroidDatabaseBuilder(p.get())

View file

@ -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<PtvGeopath>,
) {
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 = ""

View file

@ -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<PlatformDatabaseBuilder>().getBuilder()) }
single { get<Database>().getRouteDao() }
single { get<Database>().getShapeDao() }
}

View file

@ -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<Database>
}
expect fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder
internal val PlatformModule = module {
single { provideDatabaseBuilder(it) }
}

View file

@ -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,
)

View file

@ -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
}
}

View file

@ -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<Point>
@Entity
@TypeConverters(ShapeConverter::class)
data class Shape(
@PrimaryKey val id: String,
val path: ShapePath,
)

View file

@ -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<Database>) =
base.fallbackToDestructiveMigrationOnDowngrade(true)
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}
}

View file

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

View file

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

View file

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

View file

@ -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<Database> {
TODO("Not yet implemented")
}
}
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
IosDatabaseBuilder()

View file

@ -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<Database> {
val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
return Room.databaseBuilder<Database>(
name = dbFile.absolutePath,
)
}
}
actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
JvmDatabaseBuilder()