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

@ -15,15 +15,20 @@ compose-multiplatform = "1.8.0-beta02"
coroutines = "1.9.0" coroutines = "1.9.0"
geo = "0.8.0" geo = "0.8.0"
junit = "4.13.2" junit = "4.13.2"
kotlin = "2.1.10" koin = "4.1.0"
kotlin = "2.2.0"
kotlinxDatetime = "0.6.2" kotlinxDatetime = "0.6.2"
kotlinxSerializationCsv = "0.2.18"
kotlinxSerializationJson = "1.8.1" kotlinxSerializationJson = "1.8.1"
ksp = "2.2.0-2.0.2"
ktor = "3.1.1" ktor = "3.1.1"
logback = "1.5.17" logback = "1.5.17"
mapsCompose = "6.4.1" mapsCompose = "6.4.1"
okio = "3.11.0" okio = "3.11.0"
playServicesLocation = "21.3.0" playServicesLocation = "21.3.0"
playServicesMaps = "19.1.0" playServicesMaps = "19.1.0"
sqlite = "2.5.2"
room = "2.7.2"
secretsGradlePlugin = "2.0.1" secretsGradlePlugin = "2.0.1"
[libraries] [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 = { 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-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" } 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-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-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } 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" } 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-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", 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" } 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-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" } 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" } 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" } 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" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", 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" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", 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" } secretsGradle = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" }

View file

@ -1,5 +1,6 @@
plugins { plugins {
alias(libs.plugins.kotlinJvm) alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.ktor) alias(libs.plugins.ktor)
application application
} }
@ -14,8 +15,16 @@ application {
dependencies { dependencies {
implementation(projects.shared) implementation(projects.shared)
implementation(libs.logback) 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.core)
implementation(libs.ktor.server.netty) implementation(libs.ktor.server.netty)
implementation(libs.room.runtime)
implementation(libs.sqlite.bundled)
testImplementation(libs.ktor.server.tests) testImplementation(libs.ktor.server.tests)
testImplementation(libs.kotlin.test.junit) testImplementation(libs.kotlin.test.junit)
} }

View file

@ -1,11 +1,22 @@
package moe.lava.banksia.server package moe.lava.banksia.server
import io.ktor.client.HttpClient
import io.ktor.server.application.Application 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.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
import io.ktor.server.routing.get import io.ktor.server.routing.get
import io.ktor.server.routing.routing 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() { fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module) embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
@ -13,9 +24,21 @@ fun main() {
} }
fun Application.module() { fun Application.module() {
install(Koin) {
modules(module { single { log } })
modules(CommonModules, ServerModules)
}
val client = HttpClient()
routing { routing {
get("/") { get("/update") {
call.respondText("Ktor: Hi") 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<GtfsHandler>()
handler.update(datasetUrl)
}
} }
} }
} }

View file

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

View file

@ -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<GtfsRoute>()
.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<GtfsShape>()
.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<File> {
val outputs = mutableListOf<File>()
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 <T> File.parseCsv(): List<T> = this
.readText()
.replace("\uFEFF", "") // remove bom
.replace("\r\n", "\n") // crlf -> lf
.let { csv.decodeFromString(it) }
}

View file

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

View file

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

View file

@ -5,6 +5,16 @@ plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization) alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidLibrary) alias(libs.plugins.androidLibrary)
alias(libs.plugins.ksp)
alias(libs.plugins.room)
}
room {
schemaDirectory("$projectDir/schemas")
}
dependencies {
ksp(libs.room.compiler)
} }
kotlin { kotlin {
@ -27,13 +37,15 @@ kotlin {
} }
commonMain.dependencies { commonMain.dependencies {
implementation(libs.okio) implementation(libs.okio)
// put your Multiplatform dependencies here implementation(libs.koin.core)
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentnegotiation) implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.room.runtime)
implementation(libs.sqlite.bundled)
} }
iosMain.dependencies { iosMain.dependencies {
implementation(libs.ktor.client.darwin) 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.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import moe.lava.banksia.model.RouteType
// 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),
}
@Serializable @Serializable
data class PtvRoute( data class PtvRoute(
@ -24,13 +13,8 @@ data class PtvRoute(
@SerialName("route_gtfs_id") val routeGtfsId: String, @SerialName("route_gtfs_id") val routeGtfsId: String,
@SerialName("geopath") val geopath: List<PtvGeopath>, @SerialName("geopath") val geopath: List<PtvGeopath>,
) { ) {
fun gtfsSubType(): GtfsSubType? { fun gtfsSubType(): RouteType =
GtfsSubType.entries.forEach { RouteType.entries.first { routeGtfsId.startsWith(it.value.toString() + "-") }
if (routeGtfsId.startsWith(it.value.toString()))
return it
}
return null
}
fun getShortFullName(): String { fun getShortFullName(): String {
var res = "" 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()