Compare commits

...

7 commits

35 changed files with 344 additions and 217 deletions

View file

@ -2,9 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidMultiplatformLibrary) alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.ksp)
} }
kotlin { kotlin {
@ -27,26 +25,8 @@ kotlin {
jvm() jvm()
sourceSets { sourceSets {
androidMain.dependencies {
implementation(libs.koin.compose)
implementation(libs.ktor.client.okhttp)
}
commonMain.dependencies { commonMain.dependencies {
implementation(libs.okio)
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.kotlinx.serialization.protobuf)
implementation(projects.core) implementation(projects.core)
implementation(projects.core.room)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
} }
} }
} }

View file

@ -0,0 +1,54 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.ksp)
}
kotlin {
android {
namespace = "moe.lava.banksia.core.data.client"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
iosArm64()
iosSimulatorArm64()
jvm()
sourceSets {
androidMain.dependencies {
implementation(libs.koin.compose)
implementation(libs.ktor.client.okhttp)
}
commonMain.dependencies {
api(projects.core.data)
implementation(libs.okio)
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.kotlinx.serialization.protobuf)
implementation(projects.core)
implementation(projects.core.room)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}

View file

@ -8,6 +8,9 @@ import io.ktor.client.plugins.plugin
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import moe.lava.banksia.core.Constants import moe.lava.banksia.core.Constants
import moe.lava.banksia.core.data.repositories.ClientRouteRepository
import moe.lava.banksia.core.data.repositories.ClientStopRepository
import moe.lava.banksia.core.data.repositories.ClientStopTimeRepository
import moe.lava.banksia.core.data.repositories.RouteRepository import moe.lava.banksia.core.data.repositories.RouteRepository
import moe.lava.banksia.core.data.repositories.StopRepository import moe.lava.banksia.core.data.repositories.StopRepository
import moe.lava.banksia.core.data.repositories.StopTimeRepository import moe.lava.banksia.core.data.repositories.StopTimeRepository
@ -21,9 +24,10 @@ import moe.lava.banksia.core.room.roomDiModule
import moe.lava.banksia.core.util.log import moe.lava.banksia.core.util.log
import moe.lava.banksia.data.ptv.PtvService import moe.lava.banksia.data.ptv.PtvService
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
val dataDiModule = module { val clientDataDiModule = module {
includes(roomDiModule) includes(roomDiModule)
// HTTP Clients // HTTP Clients
@ -56,7 +60,7 @@ val dataDiModule = module {
singleOf(::StopTimeRemoteDataSource) singleOf(::StopTimeRemoteDataSource)
// Repositories // Repositories
singleOf(::RouteRepository) singleOf(::ClientRouteRepository) bind RouteRepository::class
singleOf(::StopRepository) singleOf(::ClientStopRepository) bind StopRepository::class
singleOf(::StopTimeRepository) singleOf(::ClientStopTimeRepository) bind StopTimeRepository::class
} }

View file

@ -0,0 +1,25 @@
package moe.lava.banksia.core.data.repositories
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource
import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource
internal class ClientRouteRepository internal constructor(
private val local: RouteLocalDataSource,
private val remote: RouteRemoteDataSource,
) : RouteRepository {
private val mutex = Mutex()
override suspend fun getAll() = mutex.withLock {
local
.getAll()
.map { it.asModel() }
.ifEmpty {
remote
.getAll()
.also { local.save(*it.toTypedArray()) }
}
}
override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
}

View file

@ -0,0 +1,22 @@
package moe.lava.banksia.core.data.repositories
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource
internal class ClientStopRepository internal constructor(
private val local: StopLocalDataSource,
private val remote: StopRemoteDataSource,
) : StopRepository {
private val mutex = Mutex()
override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
override suspend fun getByRoute(id: String) = mutex.withLock {
local
.getByRoute(id)
.map { it.asModel() }
.ifEmpty { null }
?: remote.getByRoute(id)
}
}

View file

@ -0,0 +1,16 @@
package moe.lava.banksia.core.data.repositories
import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource
import moe.lava.banksia.core.data.sources.stoptime.StopTimeRemoteDataSource
import moe.lava.banksia.core.model.StopTimeDated
internal class ClientStopTimeRepository internal constructor(
private val local: StopTimeLocalDataSource,
private val remote: StopTimeRemoteDataSource,
) : StopTimeRepository {
override suspend fun getForStop(id: String): List<StopTimeDated> {
return local
.getAtStop(id)
.ifEmpty { remote.getAtStop(id) }
}
}

View file

@ -0,0 +1,21 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
}
dependencies {
implementation(libs.okio)
implementation(libs.koin.core)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
api(projects.core.data)
implementation(projects.core)
implementation(projects.core.room)
}

View file

@ -1,25 +1,8 @@
package moe.lava.banksia.core.data.repositories package moe.lava.banksia.core.data.repositories
import kotlinx.coroutines.sync.Mutex import moe.lava.banksia.core.model.Route
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource
import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource
class RouteRepository internal constructor( interface RouteRepository {
private val local: RouteLocalDataSource, suspend fun get(id: String): Route
private val remote: RouteRemoteDataSource, suspend fun getAll(): List<Route>
) {
private val mutex = Mutex()
suspend fun getAll() = mutex.withLock {
local
.getAll()
.map { it.asModel() }
.ifEmpty {
remote
.getAll()
.also { local.save(*it.toTypedArray()) }
}
}
suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
} }

View file

@ -1,22 +1,8 @@
package moe.lava.banksia.core.data.repositories package moe.lava.banksia.core.data.repositories
import kotlinx.coroutines.sync.Mutex import moe.lava.banksia.core.model.Stop
import kotlinx.coroutines.sync.withLock
import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource
import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource
class StopRepository internal constructor( interface StopRepository {
private val local: StopLocalDataSource, suspend fun get(id: String): Stop
private val remote: StopRemoteDataSource, suspend fun getByRoute(id: String): List<Stop>
) {
private val mutex = Mutex()
suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) }
suspend fun getByRoute(id: String) = mutex.withLock {
local
.getByRoute(id)
.map { it.asModel() }
.ifEmpty { null }
?: remote.getByRoute(id)
}
} }

View file

@ -1,16 +1,7 @@
package moe.lava.banksia.core.data.repositories package moe.lava.banksia.core.data.repositories
import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource
import moe.lava.banksia.core.data.sources.stoptime.StopTimeRemoteDataSource
import moe.lava.banksia.core.model.StopTimeDated import moe.lava.banksia.core.model.StopTimeDated
class StopTimeRepository internal constructor( interface StopTimeRepository {
private val local: StopTimeLocalDataSource, suspend fun getForStop(id: String): List<StopTimeDated>
private val remote: StopTimeRemoteDataSource,
) {
suspend fun getForStop(id: String): List<StopTimeDated> {
return local
.getAtStop(id)
.ifEmpty { remote.getAtStop(id) }
}
} }

View file

@ -0,0 +1,22 @@
package moe.lava.banksia.core.room
import android.content.Context
import androidx.room.Room
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
actual class DatabaseManager actual constructor() : KoinComponent {
private val ctx by inject<Context>()
actual val database by lazy {
val ctx = get<Context>().applicationContext
val dbFile = ctx.getDatabasePath("room.db")
val builder = Room.databaseBuilder<Database>(
context = ctx,
name = dbFile.absolutePath,
)
Database.build(builder)
}
}

View file

@ -1,21 +0,0 @@
package moe.lava.banksia.core.room
import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
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
)
}
}
internal actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
AndroidDatabaseBuilder(get())

View file

@ -0,0 +1,7 @@
package moe.lava.banksia.core.room
import org.koin.core.component.KoinComponent
expect class DatabaseManager() : KoinComponent {
val database: Database
}

View file

@ -1,26 +1,18 @@
package moe.lava.banksia.core.room package moe.lava.banksia.core.room
import androidx.room.RoomDatabase import org.koin.core.module.dsl.singleOf
import org.koin.core.parameter.ParametersHolder
import org.koin.core.scope.Scope
import org.koin.dsl.module import org.koin.dsl.module
val roomDiModule = module { val roomDiModule = module {
single { provideDatabaseBuilder(it) } singleOf(::DatabaseManager)
single { Database.build(get<PlatformDatabaseBuilder>().getBuilder()) } factory { get<DatabaseManager>().database }
single { get<Database>().versionMetadataDao } factory { get<Database>().versionMetadataDao }
single { get<Database>().routeDao } factory { get<Database>().routeDao }
single { get<Database>().serviceDao } factory { get<Database>().serviceDao }
single { get<Database>().serviceExceptionDao } factory { get<Database>().serviceExceptionDao }
single { get<Database>().shapeDao } factory { get<Database>().shapeDao }
single { get<Database>().stopDao } factory { get<Database>().stopDao }
single { get<Database>().stopTimeDao } factory { get<Database>().stopTimeDao }
single { get<Database>().tripDao } factory { get<Database>().tripDao }
} }
internal interface PlatformDatabaseBuilder {
fun getBuilder(): RoomDatabase.Builder<Database>
}
internal expect fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder

View file

@ -15,7 +15,7 @@ interface StopDao {
@Query(""" @Query("""
SELECT * FROM Stop SELECT * FROM Stop
WHERE platformCode <> "" WHERE platformCode <> ""
AND parent == "" AND parent IS NULL
""") """)
suspend fun getAllParentless(): List<StopEntity> suspend fun getAllParentless(): List<StopEntity>

View file

@ -0,0 +1,8 @@
package moe.lava.banksia.core.room
import org.koin.core.component.KoinComponent
actual class DatabaseManager actual constructor() : KoinComponent {
actual val database: Database
get() = TODO("Not yet implemented")
}

View file

@ -1,14 +0,0 @@
package moe.lava.banksia.core.room
import androidx.room.RoomDatabase
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")
}
}
internal actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
IosDatabaseBuilder()

View file

@ -0,0 +1,69 @@
package moe.lava.banksia.core.room
import androidx.room.Room
import androidx.room.RoomDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import moe.lava.banksia.core.util.error
import org.koin.core.component.KoinComponent
import java.io.File
import kotlin.system.exitProcess
actual class DatabaseManager : KoinComponent {
private var liveDatabase: Database = Database.build(getBuilder())
actual val database get() = liveDatabase
private fun getBuilder(path: String = "./data/room.db"): RoomDatabase.Builder<Database> {
val dbFile = File(path)
return Room.databaseBuilder<Database>(
name = dbFile.absolutePath,
).setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
}
fun makeAlt() = Database.build(getBuilder("./data/room_alt.db"))
private fun deleteAll(file: File): Boolean {
val r1 = file.takeIf { it.exists() }?.delete()
val r2 = File(file.parentFile, file.name + ".lck").takeIf { it.exists() }?.delete()
val r3 = File(file.parentFile, file.name + "-journal").takeIf { it.exists() }?.delete()
return r1 != false && r2 != false && r3 != false
}
private fun renameAll(from: File, to: File): Boolean {
val r1 = from.takeIf { it.exists() }?.renameTo(to)
val r2 = File(from.parentFile, from.name + ".lck").takeIf { it.exists() }?.renameTo(File(to.parentFile, to.name + ".lck"))
val r3 = File(from.parentFile, from.name + "-journal").takeIf { it.exists() }?.renameTo(File(to.parentFile, to.name + "-journal"))
return r1 != false && r2 != false && r3 != false
}
fun swap(scope: CoroutineScope = CoroutineScope(Dispatchers.IO)) {
val live = File("./data/room.db")
val alt = File("./data/room_alt.db")
val old = File("./data/room_old.db")
if (!renameAll(live, old)) {
error("DatabaseManager", "Failed to rename database from live to old (${live.absolutePath} -> ${old.absolutePath})")
return
}
if (!renameAll(alt, live)) {
error("DatabaseManager", "Failed to rename database from alt to live, trying to undo.. (${alt.absolutePath} -> ${live.absolutePath})")
if (!live.renameTo(old)) {
error("DatabaseManager", "Failed to undo, critical failure, exiting..")
exitProcess(1)
}
return
}
val oldDatabase = liveDatabase
liveDatabase = Database.build(getBuilder())
scope.launch {
delay(5000)
if (!deleteAll(old)) {
error("DatabaseManager", "Failed to unlink old database, stray files! (${old.absolutePath})")
}
oldDatabase.close()
}
}
}

View file

@ -1,19 +0,0 @@
package moe.lava.banksia.core.room
import androidx.room.Room
import androidx.room.RoomDatabase
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("./data/room.db")
return Room.databaseBuilder<Database>(
name = dbFile.absolutePath,
)
}
}
internal actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder =
JvmDatabaseBuilder()

View file

@ -5,7 +5,7 @@ plugins {
application application
} }
group = "moe.lava.banksia" group = "moe.lava.banksia.server"
version = "1.0.0" version = "1.0.0"
application { application {
mainClass.set("moe.lava.banksia.server.ApplicationKt") mainClass.set("moe.lava.banksia.server.ApplicationKt")

View file

@ -135,7 +135,7 @@ class GtfsParser(
.forEach { fd -> .forEach { fd ->
log.info("parsing stop times for ${fd.parent}...") log.info("parsing stop times for ${fd.parent}...")
parseStopTimes(fd, trips) { seq -> parseStopTimes(fd, trips) { seq ->
seq.chunked(10000) seq.chunked(1000000)
.forEach { emit(GtfsData.StopTimeChunk(it)) } .forEach { emit(GtfsData.StopTimeChunk(it)) }
} }
} }
@ -170,7 +170,7 @@ class GtfsParser(
id = stop_id, id = stop_id,
name = stop_name, name = stop_name,
pos = Point(stop_lat, stop_lon), pos = Point(stop_lat, stop_lon),
parent = parent_station, parent = parent_station.ifEmpty { null },
hasWheelChairBoarding = wheelchair_boarding == "1", hasWheelChairBoarding = wheelchair_boarding == "1",
level = level_id, level = level_id,
platformCode = platform_code, platformCode = platform_code,

View file

@ -28,7 +28,7 @@ import moe.lava.banksia.core.util.serialise
import moe.lava.banksia.server.di.ServerModules import moe.lava.banksia.server.di.ServerModules
import moe.lava.banksia.server.gtfsrt.GtfsrtService import moe.lava.banksia.server.gtfsrt.GtfsrtService
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.ktor.ext.inject import org.koin.ktor.ext.get
import org.koin.ktor.plugin.Koin import org.koin.ktor.plugin.Koin
import kotlin.time.Clock import kotlin.time.Clock
@ -46,19 +46,29 @@ fun Application.module() {
modules(ServerModules) modules(ServerModules)
} }
val gtfsr by inject<GtfsrtService>()
@Suppress("KotlinConstantConditions") @Suppress("KotlinConstantConditions")
launch { gtfsr.start(this, !Constants.devMode) } launch { get<GtfsrtService>().start(this, !Constants.devMode) }
routing { routing {
if (Constants.devMode) { if (Constants.devMode) {
get("/fixup") { get("/fixup") {
call.respondText("received") call.respondText("received")
val fixer by inject<GtfsDataFixer>() get<GtfsDataFixer>().addParentsToStops()
fixer.addParentsToStops()
} }
} }
get("/update") { get("/manage/fixup") {
val key = call.parameters["key"]
if (key != Constants.updateKey) {
call.respond(HttpStatusCode.Forbidden)
return@get
}
call.respondText("fixing")
launch(context = Dispatchers.IO) {
get<GtfsDataFixer>().addParentsToStops()
}
}
get("/manage/update") {
val key = call.parameters["key"] val key = call.parameters["key"]
if (key != Constants.updateKey) { if (key != Constants.updateKey) {
call.respond(HttpStatusCode.Forbidden) call.respond(HttpStatusCode.Forbidden)
@ -70,16 +80,13 @@ fun Application.module() {
?: "https://opendata.transport.vic.gov.au/dataset/3f4e292e-7f8a-4ffe-831f-1953be0fe448/resource/${datasetUuid}/download/gtfs.zip" ?: "https://opendata.transport.vic.gov.au/dataset/3f4e292e-7f8a-4ffe-831f-1953be0fe448/resource/${datasetUuid}/download/gtfs.zip"
call.respondText("received") call.respondText("received")
launch(context = Dispatchers.IO) { launch(context = Dispatchers.IO) {
val fixer by inject<GtfsDataFixer>() get<GtfsImporter>().import(datasetUrl)
val importer by inject<GtfsImporter>() get<GtfsDataFixer>().addParentsToStops()
importer.import(datasetUrl)
fixer.addParentsToStops()
} }
} }
get("/metadata/{type?}") { get("/metadata/{type?}") {
val dao by inject<VersionMetadataDao>() val dao = get<VersionMetadataDao>()
val type = call.parameters["type"] val type = call.parameters["type"]
if (type == null) { if (type == null) {
call.respond(dao.getAll().map { it.asModel() }) call.respond(dao.getAll().map { it.asModel() })
@ -96,7 +103,7 @@ fun Application.module() {
get("/routes") { get("/routes") {
val routes = withContext(context = Dispatchers.IO) { val routes = withContext(context = Dispatchers.IO) {
inject<RouteDao>().value.getAll() get<RouteDao>().getAll()
} }
val res = routes.map { it.asModel() } val res = routes.map { it.asModel() }
call.respond(res) call.respond(res)
@ -104,7 +111,7 @@ fun Application.module() {
get("/routes/{route_id}") { get("/routes/{route_id}") {
val routeId = call.parameters["route_id"]!! val routeId = call.parameters["route_id"]!!
val route = withContext(context = Dispatchers.IO) { val route = withContext(context = Dispatchers.IO) {
inject<RouteDao>().value.get(routeId) get<RouteDao>().get(routeId)
} }
if (route != null) if (route != null)
call.respond(route.asModel()) call.respond(route.asModel())
@ -113,7 +120,7 @@ fun Application.module() {
} }
get("/stops") { get("/stops") {
val routes = withContext(context = Dispatchers.IO) { val routes = withContext(context = Dispatchers.IO) {
inject<StopDao>().value.getAll() get<StopDao>().getAll()
} }
val res = routes.map { it.asModel() } val res = routes.map { it.asModel() }
call.respond(res) call.respond(res)
@ -121,7 +128,7 @@ fun Application.module() {
get("/stops/{stop_id}") { get("/stops/{stop_id}") {
val stopId = call.parameters["stop_id"]!! val stopId = call.parameters["stop_id"]!!
val stop = withContext(context = Dispatchers.IO) { val stop = withContext(context = Dispatchers.IO) {
inject<StopDao>().value.get(stopId) get<StopDao>().get(stopId)
} }
if (stop != null) if (stop != null)
call.respond(stop.asModel()) call.respond(stop.asModel())
@ -132,7 +139,7 @@ fun Application.module() {
val routeId = call.parameters["route_id"]!! val routeId = call.parameters["route_id"]!!
val useParent = call.queryParameters["parent"] !in listOf("false", "0") val useParent = call.queryParameters["parent"] !in listOf("false", "0")
val stops = withContext(Dispatchers.IO) { val stops = withContext(Dispatchers.IO) {
val routeDao by inject<RouteDao>() val routeDao = get<RouteDao>()
if (useParent) if (useParent)
routeDao.stopsParent(routeId) routeDao.stopsParent(routeId)
else else
@ -146,7 +153,7 @@ fun Application.module() {
?.let { LocalDate.parse(it, LocalDate.Formats.ISO) } ?.let { LocalDate.parse(it, LocalDate.Formats.ISO) }
?: Clock.System.todayIn(TimeZone.currentSystemDefault()) ?: Clock.System.todayIn(TimeZone.currentSystemDefault())
val times = withContext(context = Dispatchers.IO) { val times = withContext(context = Dispatchers.IO) {
inject<StopTimeDao>().value get<StopTimeDao>()
.getForStopDated( .getForStopDated(
stopId, stopId,
listOf(date.dayOfWeek).serialise(), listOf(date.dayOfWeek).serialise(),

View file

@ -24,14 +24,14 @@ class GtfsDataFixer(
name = name, name = name,
lat = avgLat, lat = avgLat,
lng = avgLng, lng = avgLng,
parent = "", parent = null,
hasWheelChairBoarding = stops.all { it.hasWheelChairBoarding }, hasWheelChairBoarding = stops.all { it.hasWheelChairBoarding },
level = "", level = "",
platformCode = "", platformCode = "",
) )
log("datafixer", "inserting ${parentId} for ${stops.size} children") log("datafixer", "inserting ${parentId} for ${stops.size} children")
dao.insertAll(parent) dao.insertAll(parent)
database.stopDao.updateParents(stops.map { it.id }, parentId) dao.updateParents(stops.map { it.id }, parentId)
} }
} }
} }

View file

@ -1,7 +1,5 @@
package moe.lava.banksia.server package moe.lava.banksia.server
import androidx.room.immediateTransaction
import androidx.room.useWriterConnection
import io.ktor.util.logging.Logger import io.ktor.util.logging.Logger
import moe.lava.banksia.core.model.Route import moe.lava.banksia.core.model.Route
import moe.lava.banksia.core.model.Service import moe.lava.banksia.core.model.Service
@ -11,6 +9,7 @@ import moe.lava.banksia.core.model.Stop
import moe.lava.banksia.core.model.StopTime import moe.lava.banksia.core.model.StopTime
import moe.lava.banksia.core.model.Trip import moe.lava.banksia.core.model.Trip
import moe.lava.banksia.core.room.Database import moe.lava.banksia.core.room.Database
import moe.lava.banksia.core.room.DatabaseManager
import moe.lava.banksia.core.room.entity.asEntity import moe.lava.banksia.core.room.entity.asEntity
import moe.lava.banksia.server.gtfs.GtfsData import moe.lava.banksia.server.gtfs.GtfsData
import moe.lava.banksia.server.gtfs.GtfsParser import moe.lava.banksia.server.gtfs.GtfsParser
@ -18,74 +17,66 @@ import kotlin.time.Clock
class GtfsImporter( class GtfsImporter(
private val parser: GtfsParser, private val parser: GtfsParser,
private val database: Database, private val dbm: DatabaseManager,
private val log: Logger, private val log: Logger,
) { ) {
suspend fun import(url: String, date: Long = Clock.System.now().epochSeconds) { suspend fun import(url: String, date: Long = Clock.System.now().epochSeconds) {
database.useWriterConnection { transactor -> val database = dbm.makeAlt()
transactor.immediateTransaction {
database.routeDao.deleteAll()
database.serviceDao.deleteAll()
database.serviceExceptionDao.deleteAll()
database.shapeDao.deleteAll()
database.stopDao.deleteAll()
database.stopTimeDao.deleteAll()
database.tripDao.deleteAll()
parser.update(url).collect { chunk -> parser.update(url).collect { chunk ->
when (chunk) { when (chunk) {
is GtfsData.RouteChunk -> addRoutes(chunk.routes) is GtfsData.RouteChunk -> database.addRoutes(chunk.routes)
is GtfsData.ServiceChunk -> addServices(chunk.services) is GtfsData.ServiceChunk -> database.addServices(chunk.services)
is GtfsData.ServiceExceptionChunk -> addServiceExceptions(chunk.exceptions) is GtfsData.ServiceExceptionChunk -> database.addServiceExceptions(chunk.exceptions)
is GtfsData.ShapeChunk -> addShapes(chunk.shapes) is GtfsData.ShapeChunk -> database.addShapes(chunk.shapes)
is GtfsData.StopChunk -> addStops(chunk.stops) is GtfsData.StopChunk -> database.addStops(chunk.stops)
is GtfsData.StopTimeChunk -> addStopTimes(chunk.stopTimes) is GtfsData.StopTimeChunk -> database.addStopTimes(chunk.stopTimes)
is GtfsData.TripChunk -> addTrips(chunk.trips) is GtfsData.TripChunk -> database.addTrips(chunk.trips)
}
}
updateMetadata(date)
} }
} }
database.updateMetadata(date)
database.close()
dbm.swap()
} }
private suspend fun updateMetadata(date: Long) { private suspend fun Database.updateMetadata(date: Long) {
val dao = database.versionMetadataDao val dao = versionMetadataDao
log.info("updating metadata...") log.info("updating metadata...")
dao.update(date, listOf("routes", "stops", "shapes", "trips", "stop_times")) dao.update(date, listOf("routes", "stops", "shapes", "trips", "stop_times"))
log.info("done") log.info("done")
} }
private suspend fun addRoutes(routes: List<Route>) { private suspend fun Database.addRoutes(routes: List<Route>) {
val dao = database.routeDao val dao = routeDao
log.info("inserting routes...") log.info("inserting routes...")
dao.insertOrReplaceAll(*routes.map { it.asEntity() }.toTypedArray()) dao.insertOrReplaceAll(*routes.map { it.asEntity() }.toTypedArray())
log.info("done") log.info("done")
} }
private suspend fun addServices(services: List<Service>) { private suspend fun Database.addServices(services: List<Service>) {
val dao = database.serviceDao val dao = serviceDao
log.info("inserting services...") log.info("inserting services...")
dao.insertOrReplaceAll(*services.map { it.asEntity() }.toTypedArray()) dao.insertOrReplaceAll(*services.map { it.asEntity() }.toTypedArray())
log.info("done") log.info("done")
} }
private suspend fun addServiceExceptions(exceptions: List<ServiceException>) { private suspend fun Database.addServiceExceptions(exceptions: List<ServiceException>) {
val dao = database.serviceExceptionDao val dao = serviceExceptionDao
log.info("inserting exceptions...") log.info("inserting exceptions...")
dao.insertOrReplaceAll(*exceptions.map { it.asEntity() }.toTypedArray()) dao.insertOrReplaceAll(*exceptions.map { it.asEntity() }.toTypedArray())
log.info("done") log.info("done")
} }
private suspend fun addShapes(shapes: List<Shape>) { private suspend fun Database.addShapes(shapes: List<Shape>) {
val dao = database.shapeDao val dao = shapeDao
log.info("inserting shapes...") log.info("inserting shapes...")
dao.insertOrReplaceAll(*shapes.map { it.asEntity() }.toTypedArray()) dao.insertOrReplaceAll(*shapes.map { it.asEntity() }.toTypedArray())
log.info("done") log.info("done")
} }
private suspend fun addStops(stops: List<Stop>) { private suspend fun Database.addStops(stops: List<Stop>) {
val dao = database.stopDao val dao = stopDao
log.info("inserting stops...") log.info("inserting stops...")
stops stops
.groupBy { it.id } .groupBy { it.id }
@ -102,15 +93,15 @@ class GtfsImporter(
log.info("done") log.info("done")
} }
private suspend fun addStopTimes(stopTimes: List<StopTime>) { private suspend fun Database.addStopTimes(stopTimes: List<StopTime>) {
val dao = database.stopTimeDao val dao = stopTimeDao
log.info("inserting ${stopTimes.size} stoptimes...") log.info("inserting ${stopTimes.size} stoptimes...")
dao.insertOrReplaceAll(*stopTimes.map { it.asEntity() }.toTypedArray()) dao.insertOrReplaceAll(*stopTimes.map { it.asEntity() }.toTypedArray())
log.info("done") log.info("done")
} }
private suspend fun addTrips(trips: List<Trip>) { private suspend fun Database.addTrips(trips: List<Trip>) {
val dao = database.tripDao val dao = tripDao
log.info("inserting ${trips.size} trips...") log.info("inserting ${trips.size} trips...")
dao.insertOrReplaceAll(*trips.map { it.asEntity() }.toTypedArray()) dao.insertOrReplaceAll(*trips.map { it.asEntity() }.toTypedArray())
log.info("done") log.info("done")

View file

@ -6,6 +6,7 @@ import moe.lava.banksia.server.GtfsDataFixer
import moe.lava.banksia.server.GtfsImporter import moe.lava.banksia.server.GtfsImporter
import moe.lava.banksia.server.gtfs.GtfsParser import moe.lava.banksia.server.gtfs.GtfsParser
import moe.lava.banksia.server.gtfsrt.GtfsrtService import moe.lava.banksia.server.gtfsrt.GtfsrtService
import org.koin.core.module.dsl.factoryOf
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
@ -16,6 +17,6 @@ val ServerModules = module {
singleOf(::GtfsParser) singleOf(::GtfsParser)
singleOf(::GtfsrtService) singleOf(::GtfsrtService)
singleOf(::GtfsDataFixer) factoryOf(::GtfsDataFixer)
singleOf(::GtfsImporter) factoryOf(::GtfsImporter)
} }

View file

@ -37,6 +37,8 @@ include(":server:gtfs")
include(":server:gtfs_rt") include(":server:gtfs_rt")
include(":core") include(":core")
include(":core:data") include(":core:data")
include(":core:data:client")
include(":core:data:server")
include(":core:room") include(":core:room")
include(":ui") include(":ui")
include(":ui:maps") include(":ui:maps")

View file

@ -68,7 +68,7 @@ kotlin {
implementation(libs.ui.backhandler) implementation(libs.ui.backhandler)
implementation(projects.core) implementation(projects.core)
implementation(projects.core.data) implementation(projects.core.data.client)
implementation(projects.ui.maps) implementation(projects.ui.maps)
implementation(projects.ui.shared) implementation(projects.ui.shared)
} }

View file

@ -1,12 +1,12 @@
package moe.lava.banksia.ui.di package moe.lava.banksia.ui.di
import moe.lava.banksia.core.data.dataDiModule import moe.lava.banksia.core.data.clientDataDiModule
import moe.lava.banksia.ui.screens.map.MapScreenViewModel import moe.lava.banksia.ui.screens.map.MapScreenViewModel
import org.koin.core.module.dsl.viewModelOf import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module import org.koin.dsl.module
val AppModule = module { val AppModule = module {
includes(dataDiModule) includes(clientDataDiModule)
// ViewModel // ViewModel
viewModelOf(::MapScreenViewModel) viewModelOf(::MapScreenViewModel)