Skip to content

Commit

Permalink
IO: Add NSW stops proto parser and resources (#629)
Browse files Browse the repository at this point in the history
Create Proto Parser for NSW Stops

Added a proto parser to read NSW stops data from a bundled protobuf file using Compose Resources. The parser:

- Loads NSW_STOPS.pb file from compose resources
- Decodes the protobuf data into NswStopList model
- Measures and logs parsing performance metrics
- Integrates with dependency injection via Koin
- Initializes parsing in SavedTripsViewModel

Links:
- [Compose Resources Documentation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-resources-usage.html#raw-files)
  • Loading branch information
ksharma-xyz authored Feb 28, 2025
1 parent 70cfbbb commit a3cd5d2
Show file tree
Hide file tree
Showing 16 changed files with 156 additions and 9 deletions.
1 change: 1 addition & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ kotlin {
implementation(projects.feature.tripPlanner.ui)
implementation(projects.sandook)
implementation(projects.taj)
implementation(projects.io.gtfs)

implementation(libs.navigation.compose)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import xyz.ksharma.krail.core.appinfo.di.appInfoModule
import xyz.ksharma.krail.core.di.DispatchersComponent.Companion.IODispatcher
import xyz.ksharma.krail.core.di.coroutineDispatchersModule
import xyz.ksharma.krail.core.remote_config.di.remoteConfigModule
import xyz.ksharma.krail.io.gtfs.di.gtfsModule
import xyz.ksharma.krail.sandook.di.sandookModule
import xyz.ksharma.krail.splash.SplashViewModel
import xyz.ksharma.krail.trip.planner.network.api.di.networkModule
Expand All @@ -29,6 +30,7 @@ fun initKoin(config: KoinAppDeclaration? = null) {
analyticsModule,
remoteConfigModule,
coroutineDispatchersModule,
gtfsModule,
)
}
}
Expand All @@ -41,6 +43,7 @@ val splashModule = module {
appInfoProvider = get(),
remoteConfig = get(),
ioDispatcher = get(named(IODispatcher)),
protoParser = get(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -17,6 +18,7 @@ import xyz.ksharma.krail.core.analytics.event.AnalyticsEvent
import xyz.ksharma.krail.core.appinfo.AppInfoProvider
import xyz.ksharma.krail.core.log.log
import xyz.ksharma.krail.core.remote_config.RemoteConfig
import xyz.ksharma.krail.io.gtfs.nswstops.ProtoParser
import xyz.ksharma.krail.sandook.Sandook
import xyz.ksharma.krail.taj.theme.DEFAULT_THEME_STYLE
import xyz.ksharma.krail.taj.theme.KrailThemeStyle
Expand All @@ -27,6 +29,7 @@ class SplashViewModel(
private val appInfoProvider: AppInfoProvider,
private val remoteConfig: RemoteConfig,
private val ioDispatcher: CoroutineDispatcher,
private val protoParser: ProtoParser,
) : ViewModel() {

private val _uiState: MutableStateFlow<KrailThemeStyle> =
Expand All @@ -39,6 +42,13 @@ class SplashViewModel(
loadKrailThemeStyle()
trackAppStartEvent()
remoteConfig.setup() // App Start Event

kotlin.runCatching {
log("Reading NSW_STOPS proto file - StopsParser:")
protoParser.parseAndInsertStops()
}.getOrElse {
log("Error reading proto file: $it")
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)

private suspend fun trackAppStartEvent() = with(appInfoProvider.getAppInfo()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ class FakeSandook : Sandook {
stops.add(NswStops(stopId, stopName, stopLat, stopLon))
}

override fun stopsCount(): Int {
return stops.size
}

override fun insertNswStopProductClass(stopId: String, productClass: Int) {
val productClasses = stopProductClasses.getOrPut(stopId) { mutableListOf() }
productClasses.add(productClass)
Expand Down Expand Up @@ -119,4 +123,8 @@ class FakeSandook : Sandook {
stopProductClasses[stop.stopId]?.none { it in excludeProductClassList } == true
}
}

override fun insertTransaction(block: () -> Unit) {
block()
}
}
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ java = "17"
# AGP compatability https://developer.android.com/build/releases/gradle-plugin
agp = "8.8.2" # Android Gradle Plugin
kotlin = "2.1.10"
core-ktx = "1.13.0"
core-ktx = "1.15.0"
junit = "4.13.2"
android-lifecycle = "2.8.7"
activity-compose = "1.10.1"
kotlinxCollectionsImmutable = "0.3.8"
kotlinxDatetime = "0.6.2"
kotlinxIoCore = "0.6.0"
kotlinxIoCore = "0.7.0"
lifecycleViewmodelCompose = "2.8.4"
navigationCompose = "2.8.0-alpha10"
kotlinxSerializationJson = "1.8.0"
Expand Down
6 changes: 0 additions & 6 deletions gtfs-static/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
android {
namespace = "xyz.ksharma.krail.gtfs_static"

buildTypes {
debug {}

release {}
}
}

plugins {
Expand Down
File renamed without changes.
10 changes: 9 additions & 1 deletion IO/gtfs/build.gradle.kts → io/gtfs/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
android {
namespace = "xyz.ksharma.krail.io.gtfs"

buildTypes {
debug {}
release {}
}
}

plugins {
alias(libs.plugins.krail.android.library)
alias(libs.plugins.krail.kotlin.multiplatform)
alias(libs.plugins.krail.compose.multiplatform)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.wire)
}

Expand All @@ -26,6 +33,7 @@ kotlin {
dependencies {
implementation(projects.core.log)
implementation(projects.core.di)
implementation(projects.sandook)

implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.core)
Expand All @@ -35,6 +43,7 @@ kotlin {
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.datetime)
implementation(compose.runtime)
implementation(compose.components.resources)

api(libs.di.koinComposeViewmodel)
}
Expand All @@ -55,7 +64,6 @@ kotlin {
}
}


wire {
kotlin {
javaInterop = true
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package xyz.ksharma.krail.io.gtfs.di

import org.koin.core.qualifier.named
import org.koin.dsl.module
import xyz.ksharma.krail.core.di.DispatchersComponent.Companion.IODispatcher
import xyz.ksharma.krail.io.gtfs.nswstops.ProtoParser
import xyz.ksharma.krail.io.gtfs.nswstops.StopsProtoParser

val gtfsModule = module {
single<ProtoParser> {
StopsProtoParser(
ioDispatcher = get(named(IODispatcher)),
sandook = get(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package xyz.ksharma.krail.io.gtfs.nswstops

import app.krail.kgtfs.proto.NswStopList

interface ProtoParser {
/**
* Reads and decodes the NSW stops from a protobuf file, then inserts the stops into the database.
* @return The decoded `NswStopList` containing the NSW stops.
*/
suspend fun parseAndInsertStops(): NswStopList?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package xyz.ksharma.krail.io.gtfs.nswstops

import app.krail.kgtfs.proto.NswStopList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.TimeZone
import kotlinx.datetime.until
import krail.io.gtfs.generated.resources.Res
import org.jetbrains.compose.resources.ExperimentalResourceApi
import xyz.ksharma.krail.core.log.log
import xyz.ksharma.krail.sandook.Sandook

class StopsProtoParser(
private val ioDispatcher: CoroutineDispatcher,
private val sandook: Sandook,
) : ProtoParser {

/**
* Reads and decodes the NSW stops from a protobuf file, then inserts the stops into the database.
*
* @return The decoded `NswStopList` containing the NSW stops.
*/
@OptIn(ExperimentalResourceApi::class)
override suspend fun parseAndInsertStops(): NswStopList = withContext(ioDispatcher) {
var start = Clock.System.now()

val byteArray = Res.readBytes("files/NSW_STOPS.pb")
val decodedStops = NswStopList.ADAPTER.decode(byteArray)

var duration = start.until(
Clock.System.now(), DateTimeUnit.MILLISECOND,
TimeZone.currentSystemDefault(),
)
log("Decoded #Stops: ${decodedStops.nswStops.size} - duration: $duration ms")

log("Start inserting stops. Currently ${sandook.stopsCount()} stops in the database")
start = Clock.System.now()
insertStopsInTransaction(decodedStops)
duration = start.until(
Clock.System.now(), DateTimeUnit.MILLISECOND, TimeZone.currentSystemDefault()
)
log("Inserted #Stops: ${decodedStops.nswStops.size} in duration: $duration ms")

decodedStops
}

private suspend fun insertStopsInTransaction(decoded: NswStopList) = withContext(ioDispatcher) {
val start = Clock.System.now()
sandook.insertTransaction {
decoded.nswStops.forEach { nswStop ->
sandook.insertNswStop(
stopId = nswStop.stopId,
stopName = nswStop.stopName,
stopLat = nswStop.lat,
stopLon = nswStop.lon
)
nswStop.productClass.forEach { productClass ->
sandook.insertNswStopProductClass(
stopId = nswStop.stopId,
productClass = productClass
)
}
}
}

val duration = start.until(
Clock.System.now(), DateTimeUnit.MILLISECOND, TimeZone.currentSystemDefault(),
)
// Log less frequently, for example once after the transaction completes
println("Inserted ${decoded.nswStops.size} stops in a single transaction in $duration ms")
// TODO - analytics track how much time it took to insert stops.
// Also track based on Firebase for performance.
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ internal class RealSandook(factory: SandookDriverFactory) : Sandook {
)
}

override fun stopsCount(): Int {
return nswStopsQueries.selectStopsCount().executeAsOne().toInt()
}

override fun insertNswStopProductClass(stopId: String, productClass: Int) {
nswStopsQueries.insertStopProductClass(stopId, productClass.toLong())
}
Expand Down Expand Up @@ -142,5 +146,9 @@ internal class RealSandook(factory: SandookDriverFactory) : Sandook {
).executeAsList()
}

override fun insertTransaction(block: () -> Unit) {
nswStopsQueries.transaction { block() }
}

// endregion NswStops
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ interface Sandook {
// region NswStops
fun insertNswStop(stopId: String, stopName: String, stopLat: Double, stopLon: Double)

fun stopsCount(): Int

fun insertNswStopProductClass(stopId: String, productClass: Int)

fun selectStopsByPartialName(stopName: String): List<NswStops>
Expand Down Expand Up @@ -72,5 +74,10 @@ interface Sandook {
excludeProductClassList: List<Int> = emptyList(),
): List<NswStops>

/**
* Inserts a list of stops in a single transaction.
*/
fun insertTransaction(block: () -> Unit)

// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ insertStop:
INSERT OR IGNORE INTO NswStops(stopId, stopName, stopLat, stopLon)
VALUES (?, ?, ?, ?);

-- Select count of items in NswStops
selectStopsCount:
SELECT COUNT(*) AS totalStops
FROM NswStops;

-- Insert each productClass value for a given stop into NswStopProductClass --
insertStopProductClass:
INSERT INTO NswStopProductClass(stopId, productClass)
Expand Down

0 comments on commit a3cd5d2

Please sign in to comment.