feat: di, db, and preliminary server-side gtfs parsing
This commit is contained in:
parent
ccc748dc1f
commit
6770c01613
22 changed files with 555 additions and 24 deletions
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
72
shared/schemas/moe.lava.banksia.room.Database/1.json
Normal file
72
shared/schemas/moe.lava.banksia.room.Database/1.json
Normal 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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
|
@ -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 = ""
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
|
}
|
||||||
|
|
@ -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) }
|
||||||
|
}
|
||||||
12
shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt
Normal file
12
shared/src/commonMain/kotlin/moe/lava/banksia/model/Route.kt
Normal 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,
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
16
shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt
Normal file
16
shared/src/commonMain/kotlin/moe/lava/banksia/model/Shape.kt
Normal 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,
|
||||||
|
)
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue